feat(hot-runner): implement hot runner protocol with registry, health monitoring, and job dispatch (#791)

Adds persistent Unity editor instance support to reduce build iteration time
by eliminating cold-start overhead. Includes:

- HotRunnerTypes: interfaces for config, status, job request/result, transport
- HotRunnerRegistry: in-memory runner management with file-based persistence
- HotRunnerHealthMonitor: periodic health checks, idle recycling, job-count recycling
- HotRunnerDispatcher: job routing with wait-for-runner, timeout, and output streaming
- HotRunnerService: high-level API integrating registry, health, and dispatch
- 34 unit tests covering registration, filtering, health, dispatch, timeout, fallback
- action.yml inputs for hot runner configuration (7 new inputs)
- Input/BuildParameters integration for hot runner settings
- index.ts wiring with cold-build fallback when hot runner unavailable

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
frostebite
2026-03-05 11:50:31 +00:00
parent 49b37f7831
commit 1bb31f3e98
13 changed files with 2170 additions and 23 deletions
Generated Vendored
+688 -10
View File
@@ -38,6 +38,7 @@ const model_1 = __nccwpck_require__(41359);
const cli_1 = __nccwpck_require__(55651);
const mac_builder_1 = __importDefault(__nccwpck_require__(39364));
const platform_setup_1 = __importDefault(__nccwpck_require__(64423));
const hot_runner_1 = __nccwpck_require__(74283);
async function runMain() {
try {
if (cli_1.Cli.InitCliMode()) {
@@ -50,17 +51,42 @@ async function runMain() {
const buildParameters = await model_1.BuildParameters.create();
const baseImage = new model_1.ImageTag(buildParameters);
let exitCode = -1;
if (buildParameters.providerStrategy === 'local') {
// Hot runner path: attempt to use a persistent Unity editor instance
if (buildParameters.hotRunnerEnabled) {
core.info('[HotRunner] Hot runner mode enabled, attempting hot build...');
const hotRunnerConfig = {
enabled: true,
transport: buildParameters.hotRunnerTransport,
host: buildParameters.hotRunnerHost,
port: buildParameters.hotRunnerPort,
healthCheckInterval: buildParameters.hotRunnerHealthInterval,
maxIdleTime: buildParameters.hotRunnerMaxIdle,
maxJobsBeforeRecycle: 0, // no automatic recycle by job count
};
const hotRunnerService = new hot_runner_1.HotRunnerService();
try {
await hotRunnerService.initialize(hotRunnerConfig);
const result = await hotRunnerService.submitBuild(buildParameters, (output) => {
core.info(output);
});
exitCode = result.exitCode;
core.info(`[HotRunner] Build completed with exit code ${exitCode}`);
await hotRunnerService.shutdown();
}
catch (hotRunnerError) {
await hotRunnerService.shutdown();
if (buildParameters.hotRunnerFallbackToCold) {
core.warning(`[HotRunner] Hot runner failed: ${hotRunnerError.message}. Falling back to cold build.`);
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
}
else {
throw hotRunnerError;
}
}
}
else if (buildParameters.providerStrategy === 'local') {
core.info('Building locally');
await platform_setup_1.default.setup(buildParameters, actionFolder);
exitCode =
process.platform === 'darwin'
? await mac_builder_1.default.run(actionFolder)
: await model_1.Docker.run(baseImage.toString(), {
workspace,
actionFolder,
...buildParameters,
});
exitCode = await runColdBuild(buildParameters, baseImage, workspace, actionFolder);
}
else {
await model_1.Orchestrator.run(buildParameters, baseImage.toString());
@@ -78,6 +104,23 @@ async function runMain() {
core.setFailed(error.message);
}
}
async function runColdBuild(buildParameters, baseImage, workspace, actionFolder) {
if (buildParameters.providerStrategy === 'local') {
core.info('Building locally');
await platform_setup_1.default.setup(buildParameters, actionFolder);
return process.platform === 'darwin'
? await mac_builder_1.default.run(actionFolder)
: await model_1.Docker.run(baseImage.toString(), {
workspace,
actionFolder,
...buildParameters,
});
}
else {
await model_1.Orchestrator.run(buildParameters, baseImage.toString());
return 0;
}
}
runMain();
@@ -375,6 +418,13 @@ class BuildParameters {
cacheUnityInstallationOnMac: input_1.default.cacheUnityInstallationOnMac,
unityHubVersionOnMac: input_1.default.unityHubVersionOnMac,
dockerWorkspacePath: input_1.default.dockerWorkspacePath,
hotRunnerEnabled: input_1.default.hotRunnerEnabled,
hotRunnerTransport: input_1.default.hotRunnerTransport,
hotRunnerHost: input_1.default.hotRunnerHost,
hotRunnerPort: input_1.default.hotRunnerPort,
hotRunnerHealthInterval: input_1.default.hotRunnerHealthInterval,
hotRunnerMaxIdle: input_1.default.hotRunnerMaxIdle,
hotRunnerFallbackToCold: input_1.default.hotRunnerFallbackToCold,
};
}
static parseBuildFile(filename, platform, androidExportType) {
@@ -1826,6 +1876,29 @@ class Input {
static get skipActivation() {
return Input.getInput('skipActivation')?.toLowerCase() ?? 'false';
}
static get hotRunnerEnabled() {
const input = Input.getInput('hotRunnerEnabled') ?? false;
return input === 'true';
}
static get hotRunnerTransport() {
return (Input.getInput('hotRunnerTransport') ?? 'websocket');
}
static get hotRunnerHost() {
return Input.getInput('hotRunnerHost') ?? 'localhost';
}
static get hotRunnerPort() {
return Number.parseInt(Input.getInput('hotRunnerPort') ?? '9090', 10);
}
static get hotRunnerHealthInterval() {
return Number.parseInt(Input.getInput('hotRunnerHealthInterval') ?? '30', 10);
}
static get hotRunnerMaxIdle() {
return Number.parseInt(Input.getInput('hotRunnerMaxIdle') ?? '3600', 10);
}
static get hotRunnerFallbackToCold() {
const input = Input.getInput('hotRunnerFallbackToCold') ?? 'true';
return input === 'true';
}
static ToEnvVarFormat(input) {
if (input.toUpperCase() === input) {
return input;
@@ -9632,6 +9705,611 @@ class ContainerHookService {
exports.ContainerHookService = ContainerHookService;
/***/ }),
/***/ 62984:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.HotRunnerDispatcher = void 0;
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
const POLL_INTERVAL_MS = 1000;
class HotRunnerDispatcher {
constructor(transports) {
this.transports = transports;
}
/**
* Dispatch a job to an available hot runner matching the request's build target.
* If no runner is immediately available, waits up to the request timeout.
* Returns the job result, or throws if no runner becomes available in time.
*/
async dispatchJob(request, registry, unityVersion, onOutput) {
orchestrator_logger_1.default.log(`[HotRunner] Dispatching job ${request.jobId} (target: ${request.buildTarget})`);
// Find or wait for an available runner
let runner = registry.findAvailableRunner({
unityVersion,
platform: request.buildTarget,
});
if (!runner) {
orchestrator_logger_1.default.log(`[HotRunner] No idle runner available for ${unityVersion}/${request.buildTarget}, waiting...`);
runner = await this.waitForRunner({ unityVersion, platform: request.buildTarget }, request.timeout, registry);
}
// Mark runner as busy
registry.updateRunner(runner.id, {
state: 'busy',
currentJob: request.jobId,
});
const transport = this.transports.get(runner.id);
if (!transport) {
registry.updateRunner(runner.id, { state: 'idle', currentJob: undefined });
throw new Error(`[HotRunner] No transport available for runner ${runner.id}`);
}
orchestrator_logger_1.default.log(`[HotRunner] Sending job ${request.jobId} to runner ${runner.id}`);
const startTime = Date.now();
try {
const result = await this.executeWithTimeout(transport, request);
const duration = Date.now() - startTime;
orchestrator_logger_1.default.log(`[HotRunner] Job ${request.jobId} completed on runner ${runner.id} in ${duration}ms (exit: ${result.exitCode})`);
if (onOutput && result.output) {
onOutput(result.output);
}
// Mark runner as idle and increment job count
const currentStatus = registry.getRunner(runner.id);
registry.updateRunner(runner.id, {
state: 'idle',
currentJob: undefined,
lastJobCompleted: request.jobId,
jobsCompleted: (currentStatus?.jobsCompleted ?? 0) + 1,
});
return result;
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[HotRunner] Job ${request.jobId} failed on runner ${runner.id}: ${error.message}`);
// Mark runner as idle despite failure -- the health monitor will recycle if needed
registry.updateRunner(runner.id, {
state: 'idle',
currentJob: undefined,
});
throw error;
}
}
/**
* Wait for an available runner matching the requirements.
* Polls the registry at a fixed interval until one becomes available or timeout expires.
*/
async waitForRunner(requirements, timeoutMs, registry) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const runner = registry.findAvailableRunner(requirements);
if (runner) {
orchestrator_logger_1.default.log(`[HotRunner] Runner ${runner.id} became available`);
return runner;
}
await this.sleep(Math.min(POLL_INTERVAL_MS, deadline - Date.now()));
}
throw new Error(`[HotRunner] Timed out waiting for available runner (${requirements.unityVersion}/${requirements.platform}) after ${timeoutMs}ms`);
}
/**
* Execute a job on a transport with a timeout guard.
*/
async executeWithTimeout(transport, request) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => {
reject(new Error(`[HotRunner] Job ${request.jobId} timed out after ${request.timeout}ms`));
}, request.timeout);
transport
.sendJob(request)
.then((result) => {
clearTimeout(timer);
resolve(result);
})
.catch((error) => {
clearTimeout(timer);
reject(error);
});
});
}
sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
}
exports.HotRunnerDispatcher = HotRunnerDispatcher;
/***/ }),
/***/ 9991:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.HotRunnerHealthMonitor = void 0;
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
class HotRunnerHealthMonitor {
constructor() {
this.transports = new Map();
}
/**
* Start periodic health monitoring for all registered runners.
*/
startMonitoring(registry, interval, transports) {
if (this.intervalHandle) {
this.stopMonitoring();
}
this.registry = registry;
this.transports = transports;
orchestrator_logger_1.default.log(`[HotRunner] Starting health monitoring (interval: ${interval}s)`);
this.intervalHandle = setInterval(() => {
this.runHealthChecks().catch((error) => {
orchestrator_logger_1.default.logWarning(`[HotRunner] Health check cycle failed: ${error.message}`);
});
}, interval * 1000);
}
/**
* Stop periodic health monitoring.
*/
stopMonitoring() {
if (this.intervalHandle) {
clearInterval(this.intervalHandle);
this.intervalHandle = undefined;
orchestrator_logger_1.default.log(`[HotRunner] Health monitoring stopped`);
}
}
/**
* Check health of a specific runner by ID. Returns true if healthy.
*/
async checkHealth(runnerId) {
if (!this.registry) {
return false;
}
const transport = this.transports.get(runnerId);
if (!transport) {
orchestrator_logger_1.default.logWarning(`[HotRunner] No transport for runner ${runnerId}`);
this.registry.updateRunner(runnerId, {
state: 'unhealthy',
lastHealthCheck: new Date().toISOString(),
});
return false;
}
try {
const healthy = await transport.healthCheck();
if (healthy) {
const status = await transport.getStatus();
this.registry.updateRunner(runnerId, {
lastHealthCheck: new Date().toISOString(),
memoryUsageMB: status.memoryUsageMB,
uptime: status.uptime,
libraryHash: status.libraryHash,
});
return true;
}
orchestrator_logger_1.default.logWarning(`[HotRunner] Runner ${runnerId} health check returned false`);
this.registry.updateRunner(runnerId, {
state: 'unhealthy',
lastHealthCheck: new Date().toISOString(),
});
return false;
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[HotRunner] Runner ${runnerId} health check failed: ${error.message}`);
this.registry.updateRunner(runnerId, {
state: 'unhealthy',
lastHealthCheck: new Date().toISOString(),
});
return false;
}
}
/**
* Mark an unhealthy runner for cleanup and disconnect its transport.
*/
async recycleUnhealthyRunner(runnerId) {
if (!this.registry) {
return;
}
orchestrator_logger_1.default.log(`[HotRunner] Recycling unhealthy runner ${runnerId}`);
this.registry.updateRunner(runnerId, { state: 'stopping' });
const transport = this.transports.get(runnerId);
if (transport) {
try {
await transport.disconnect();
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[HotRunner] Error disconnecting runner ${runnerId}: ${error.message}`);
}
this.transports.delete(runnerId);
}
this.registry.unregisterRunner(runnerId);
orchestrator_logger_1.default.log(`[HotRunner] Runner ${runnerId} recycled and removed`);
}
/**
* Recycle a runner that has been idle longer than the maximum idle time.
*/
async recycleIdleRunner(runnerId, maxIdleTime) {
if (!this.registry) {
return;
}
const runner = this.registry.getRunner(runnerId);
if (!runner || runner.state !== 'idle') {
return;
}
const lastCheckTime = new Date(runner.lastHealthCheck).getTime();
const now = Date.now();
const idleSeconds = (now - lastCheckTime) / 1000;
if (idleSeconds >= maxIdleTime) {
orchestrator_logger_1.default.log(`[HotRunner] Runner ${runnerId} idle for ${Math.floor(idleSeconds)}s (max: ${maxIdleTime}s), recycling`);
await this.recycleUnhealthyRunner(runnerId);
}
}
/**
* Run health checks and idle-recycle checks for all registered runners.
*/
async runHealthChecks() {
if (!this.registry) {
return;
}
const runners = this.registry.listRunners();
for (const runner of runners) {
if (runner.state === 'stopping') {
continue;
}
const healthy = await this.checkHealth(runner.id);
if (!healthy && runner.state !== 'starting') {
await this.recycleUnhealthyRunner(runner.id);
continue;
}
// Check for idle timeout
const config = this.registry.getConfig(runner.id);
if (config && runner.state === 'idle') {
await this.recycleIdleRunner(runner.id, config.maxIdleTime);
}
// Check for max jobs before recycle
if (config && config.maxJobsBeforeRecycle > 0 && runner.jobsCompleted >= config.maxJobsBeforeRecycle) {
orchestrator_logger_1.default.log(`[HotRunner] Runner ${runner.id} reached max jobs (${runner.jobsCompleted}/${config.maxJobsBeforeRecycle}), recycling`);
await this.recycleUnhealthyRunner(runner.id);
}
}
}
/**
* Whether health monitoring is currently active.
*/
get isMonitoring() {
return this.intervalHandle !== undefined;
}
}
exports.HotRunnerHealthMonitor = HotRunnerHealthMonitor;
/***/ }),
/***/ 12722:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.HotRunnerRegistry = void 0;
const node_fs_1 = __importDefault(__nccwpck_require__(87561));
const node_path_1 = __importDefault(__nccwpck_require__(49411));
const nanoid_1 = __nccwpck_require__(17592);
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
const generateId = (0, nanoid_1.customAlphabet)('abcdefghijklmnopqrstuvwxyz0123456789', 12);
const PERSISTENCE_FILENAME = 'hot-runners.json';
class HotRunnerRegistry {
constructor(persistenceDirectory) {
this.runners = new Map();
this.configs = new Map();
this.persistencePath = persistenceDirectory ? node_path_1.default.join(persistenceDirectory, PERSISTENCE_FILENAME) : '';
}
/**
* Register a new hot runner. Returns the generated runner ID.
*/
registerRunner(config) {
const id = `hr-${generateId()}`;
const status = {
id,
state: 'starting',
unityVersion: config.unityVersion ?? 'unknown',
platform: config.platform ?? 'unknown',
uptime: 0,
jobsCompleted: 0,
lastHealthCheck: new Date().toISOString(),
memoryUsageMB: 0,
};
this.runners.set(id, status);
this.configs.set(id, config);
orchestrator_logger_1.default.log(`[HotRunner] Registered runner ${id} (${status.unityVersion}/${status.platform})`);
this.persist();
return id;
}
/**
* Remove a runner from the registry.
*/
unregisterRunner(id) {
const existed = this.runners.delete(id);
this.configs.delete(id);
if (existed) {
orchestrator_logger_1.default.log(`[HotRunner] Unregistered runner ${id}`);
this.persist();
}
}
/**
* Get a runner's current status by ID.
*/
getRunner(id) {
return this.runners.get(id);
}
/**
* Get a runner's config by ID.
*/
getConfig(id) {
return this.configs.get(id);
}
/**
* List all runners, optionally filtered by platform, state, or Unity version.
*/
listRunners(filter) {
let results = [...this.runners.values()];
if (filter?.platform) {
results = results.filter((runner) => runner.platform === filter.platform);
}
if (filter?.state) {
results = results.filter((runner) => runner.state === filter.state);
}
if (filter?.unityVersion) {
results = results.filter((runner) => runner.unityVersion === filter.unityVersion);
}
return results;
}
/**
* Find an idle runner matching the given Unity version and platform requirements.
*/
findAvailableRunner(requirements) {
return this.listRunners({
state: 'idle',
unityVersion: requirements.unityVersion,
platform: requirements.platform,
})[0];
}
/**
* Update a runner's status fields. Merges partial updates into existing status.
*/
updateRunner(id, update) {
const existing = this.runners.get(id);
if (!existing) {
return;
}
this.runners.set(id, { ...existing, ...update, id });
this.persist();
}
/**
* Get the total number of registered runners.
*/
get size() {
return this.runners.size;
}
/**
* Persist current registry state to disk for crash recovery.
*/
persist() {
if (!this.persistencePath) {
return;
}
try {
const data = {
runners: Object.fromEntries(this.runners),
configs: Object.fromEntries(this.configs),
};
const directory = node_path_1.default.dirname(this.persistencePath);
if (!node_fs_1.default.existsSync(directory)) {
node_fs_1.default.mkdirSync(directory, { recursive: true });
}
node_fs_1.default.writeFileSync(this.persistencePath, JSON.stringify(data, undefined, 2));
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[HotRunner] Failed to persist registry: ${error.message}`);
}
}
/**
* Load registry state from disk. Returns the number of runners restored.
*/
loadFromDisk() {
if (!this.persistencePath || !node_fs_1.default.existsSync(this.persistencePath)) {
return 0;
}
try {
const raw = node_fs_1.default.readFileSync(this.persistencePath, 'utf8');
const data = JSON.parse(raw);
if (data.runners) {
for (const [id, status] of Object.entries(data.runners)) {
this.runners.set(id, status);
}
}
if (data.configs) {
for (const [id, config] of Object.entries(data.configs)) {
this.configs.set(id, config);
}
}
orchestrator_logger_1.default.log(`[HotRunner] Restored ${this.runners.size} runner(s) from disk`);
return this.runners.size;
}
catch (error) {
orchestrator_logger_1.default.logWarning(`[HotRunner] Failed to load registry from disk: ${error.message}`);
return 0;
}
}
}
exports.HotRunnerRegistry = HotRunnerRegistry;
/***/ }),
/***/ 42517:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.HotRunnerService = void 0;
const orchestrator_logger_1 = __importDefault(__nccwpck_require__(32549));
const hot_runner_registry_1 = __nccwpck_require__(12722);
const hot_runner_health_monitor_1 = __nccwpck_require__(9991);
const hot_runner_dispatcher_1 = __nccwpck_require__(62984);
class HotRunnerService {
constructor(persistenceDirectory) {
this.transports = new Map();
this.registry = new hot_runner_registry_1.HotRunnerRegistry(persistenceDirectory);
this.healthMonitor = new hot_runner_health_monitor_1.HotRunnerHealthMonitor();
this.dispatcher = new hot_runner_dispatcher_1.HotRunnerDispatcher(this.transports);
}
/**
* Initialize the hot runner service: load persisted state, start health monitoring.
*/
async initialize(config) {
this.config = config;
orchestrator_logger_1.default.log(`[HotRunner] Initializing service (transport: ${config.transport}, ${config.host}:${config.port})`);
// Attempt to restore previously registered runners from disk
const restored = this.registry.loadFromDisk();
if (restored > 0) {
orchestrator_logger_1.default.log(`[HotRunner] Restored ${restored} runner(s) from persistence`);
}
// Start health monitoring
this.healthMonitor.startMonitoring(this.registry, config.healthCheckInterval, this.transports);
orchestrator_logger_1.default.log(`[HotRunner] Service initialized`);
}
/**
* Register a runner with a transport implementation.
* Returns the runner ID.
*/
registerRunner(config, transport) {
const id = this.registry.registerRunner(config);
this.transports.set(id, transport);
return id;
}
/**
* Submit a build job to an available hot runner.
* Converts BuildParameters to a HotRunnerJobRequest and dispatches.
*/
async submitBuild(params, onOutput) {
const request = {
jobId: params.buildGuid || `build-${Date.now()}`,
buildMethod: params.buildMethod || undefined,
buildTarget: params.targetPlatform,
buildPath: params.buildPath,
customParameters: params.customParameters ? this.parseCustomParameters(params.customParameters) : undefined,
timeout: 30 * 60 * 1000, // 30 minutes default
};
orchestrator_logger_1.default.log(`[HotRunner] Submitting build: ${request.jobId} (target: ${request.buildTarget})`);
return this.dispatcher.dispatchJob(request, this.registry, params.editorVersion, onOutput);
}
/**
* Submit a test job to an available hot runner.
* Converts BuildParameters and optional suite config to a test-mode HotRunnerJobRequest.
*/
async submitTest(params, suiteConfig, onOutput) {
const request = {
jobId: params.buildGuid || `test-${Date.now()}`,
buildTarget: params.targetPlatform,
customParameters: params.customParameters ? this.parseCustomParameters(params.customParameters) : undefined,
timeout: 30 * 60 * 1000,
testMode: suiteConfig?.testMode ?? 'editmode',
testSuitePath: suiteConfig?.testSuitePath,
};
orchestrator_logger_1.default.log(`[HotRunner] Submitting test: ${request.jobId} (mode: ${request.testMode})`);
return this.dispatcher.dispatchJob(request, this.registry, params.editorVersion, onOutput);
}
/**
* Shut down the service: stop health monitoring, disconnect all transports,
* and unregister all runners.
*/
async shutdown() {
orchestrator_logger_1.default.log(`[HotRunner] Shutting down service`);
this.healthMonitor.stopMonitoring();
const disconnectPromises = [];
for (const [id, transport] of this.transports.entries()) {
disconnectPromises.push(transport.disconnect().catch((error) => {
orchestrator_logger_1.default.logWarning(`[HotRunner] Error disconnecting runner ${id}: ${error.message}`);
}));
}
await Promise.all(disconnectPromises);
this.transports.clear();
orchestrator_logger_1.default.log(`[HotRunner] Service shut down`);
}
/**
* Get the status of all registered runners.
*/
getStatus() {
return this.registry.listRunners();
}
/**
* Get the underlying registry (for testing or advanced use).
*/
getRegistry() {
return this.registry;
}
/**
* Parse a space-separated custom parameters string into a key-value map.
* Handles `-key value` and `-key=value` formats.
*/
parseCustomParameters(raw) {
const result = {};
const parts = raw.trim().split(/\s+/);
for (let i = 0; i < parts.length; i++) {
const part = parts[i];
if (part.startsWith('-')) {
const key = part.replace(/^-+/, '');
if (key.includes('=')) {
const [k, ...v] = key.split('=');
result[k] = v.join('=');
}
else if (i + 1 < parts.length && !parts[i + 1].startsWith('-')) {
result[key] = parts[i + 1];
i++;
}
else {
result[key] = 'true';
}
}
}
return result;
}
}
exports.HotRunnerService = HotRunnerService;
/***/ }),
/***/ 74283:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {
"use strict";
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.HotRunnerDispatcher = exports.HotRunnerHealthMonitor = exports.HotRunnerRegistry = exports.HotRunnerService = void 0;
var hot_runner_service_1 = __nccwpck_require__(42517);
Object.defineProperty(exports, "HotRunnerService", ({ enumerable: true, get: function () { return hot_runner_service_1.HotRunnerService; } }));
var hot_runner_registry_1 = __nccwpck_require__(12722);
Object.defineProperty(exports, "HotRunnerRegistry", ({ enumerable: true, get: function () { return hot_runner_registry_1.HotRunnerRegistry; } }));
var hot_runner_health_monitor_1 = __nccwpck_require__(9991);
Object.defineProperty(exports, "HotRunnerHealthMonitor", ({ enumerable: true, get: function () { return hot_runner_health_monitor_1.HotRunnerHealthMonitor; } }));
var hot_runner_dispatcher_1 = __nccwpck_require__(62984);
Object.defineProperty(exports, "HotRunnerDispatcher", ({ enumerable: true, get: function () { return hot_runner_dispatcher_1.HotRunnerDispatcher; } }));
/***/ }),
/***/ 23451:
Generated Vendored
+1 -1
View File
File diff suppressed because one or more lines are too long