everything-dev 0.2.1 → 0.3.1

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.
@@ -1,459 +1,549 @@
1
- import { Command } from "@effect/platform";
2
- import { Deferred, Effect, Fiber, Ref, Stream } from "every-plugin/effect";
1
+ import { Deferred, Effect, Fiber, Ref } from "every-plugin/effect";
3
2
  import type { ProcessStatus } from "../components/dev-view";
4
- import { type BosConfig, getConfigDir, getPortsFromConfig, type RemoteConfig, type SourceMode } from "../config";
3
+ import {
4
+ type BosConfig,
5
+ getProjectRoot,
6
+ parsePort,
7
+ type RemoteConfig,
8
+ type SourceMode,
9
+ } from "../config";
5
10
  import type { RuntimeConfig } from "../types";
6
11
 
7
12
  export interface DevProcess {
8
- name: string;
9
- command: string;
10
- args: string[];
11
- cwd: string;
12
- env?: Record<string, string>;
13
- port: number;
14
- readyPatterns: RegExp[];
15
- errorPatterns: RegExp[];
13
+ name: string;
14
+ command: string;
15
+ args: string[];
16
+ cwd: string;
17
+ env?: Record<string, string>;
18
+ port: number;
19
+ readyPatterns: RegExp[];
20
+ errorPatterns: RegExp[];
16
21
  }
17
22
 
18
23
  interface ProcessConfigBase {
19
- name: string;
20
- command: string;
21
- args: string[];
22
- cwd: string;
23
- readyPatterns: RegExp[];
24
- errorPatterns: RegExp[];
24
+ name: string;
25
+ command: string;
26
+ args: string[];
27
+ cwd: string;
28
+ readyPatterns: RegExp[];
29
+ errorPatterns: RegExp[];
25
30
  }
26
31
 
27
32
  const processConfigBases: Record<string, ProcessConfigBase> = {
28
- host: {
29
- name: "host",
30
- command: "bun",
31
- args: ["run", "dev"],
32
- cwd: "host",
33
- readyPatterns: [/listening on/i, /server started/i, /ready/i, /running at/i],
34
- errorPatterns: [/error:/i, /failed/i, /exception/i],
35
- },
36
- "ui-ssr": {
37
- name: "ui-ssr",
38
- command: "bun",
39
- args: ["run", "build:ssr", "--watch"],
40
- cwd: "ui",
41
- readyPatterns: [/built in/i, /compiled.*successfully/i],
42
- errorPatterns: [/error/i, /failed/i],
43
- },
44
- ui: {
45
- name: "ui",
46
- command: "bun",
47
- args: ["run", "dev"],
48
- cwd: "ui",
49
- readyPatterns: [/ready in/i, /compiled.*successfully/i, /➜.*local:/i],
50
- errorPatterns: [/error/i, /failed to compile/i],
51
- },
52
- api: {
53
- name: "api",
54
- command: "bun",
55
- args: ["run", "dev"],
56
- cwd: "api",
57
- readyPatterns: [/compiled.*successfully/i, /listening/i, /started/i],
58
- errorPatterns: [/error/i, /failed/i],
59
- },
33
+ host: {
34
+ name: "host",
35
+ command: "bun",
36
+ args: ["run", "dev"],
37
+ cwd: "host",
38
+ readyPatterns: [/Host (dev|production) server running at/i],
39
+ errorPatterns: [/error:/i, /failed/i, /exception/i],
40
+ },
41
+ ui: {
42
+ name: "ui",
43
+ command: "bun",
44
+ args: ["run", "dev"],
45
+ cwd: "ui",
46
+ readyPatterns: [/ready in/i, /compiled.*successfully/i, /➜.*local:/i],
47
+ errorPatterns: [/error/i, /failed to compile/i],
48
+ },
49
+ "ui-ssr": {
50
+ name: "ui-ssr",
51
+ command: "bun",
52
+ args: ["run", "build:ssr", "--watch"],
53
+ cwd: "ui",
54
+ readyPatterns: [/compiled.*successfully/i, /built in/i],
55
+ errorPatterns: [/error/i, /failed/i],
56
+ },
57
+ api: {
58
+ name: "api",
59
+ command: "bun",
60
+ args: ["run", "dev"],
61
+ cwd: "api",
62
+ readyPatterns: [/Plugin dev server ready/i, /listening/i, /started/i],
63
+ errorPatterns: [/error/i, /failed/i],
64
+ },
60
65
  };
61
66
 
62
67
  export const getProcessConfig = (
63
- pkg: string,
64
- env?: Record<string, string>,
65
- portOverride?: number
68
+ pkg: string,
69
+ env?: Record<string, string>,
70
+ portOverride?: number,
71
+ bosConfig?: BosConfig,
66
72
  ): DevProcess | null => {
67
- const base = processConfigBases[pkg];
68
- if (!base) return null;
69
-
70
- const ports = getPortsFromConfig();
71
-
72
- let port: number;
73
- if (pkg === "host") {
74
- port = portOverride ?? ports.host;
75
- } else if (pkg === "ui" || pkg === "ui-ssr") {
76
- port = pkg === "ui-ssr" ? 0 : ports.ui;
77
- } else if (pkg === "api") {
78
- port = ports.api;
79
- } else {
80
- port = 0;
81
- }
82
-
83
- const processEnv = pkg === "ui-ssr"
84
- ? { ...env, BUILD_TARGET: "server" }
85
- : env;
86
-
87
- return { ...base, port, env: processEnv };
73
+ const base = processConfigBases[pkg];
74
+ if (!base) return null;
75
+
76
+ // Use config if provided, otherwise defaults
77
+ let port: number;
78
+ if (pkg === "host") {
79
+ port =
80
+ portOverride ??
81
+ (bosConfig ? parsePort(bosConfig.app.host.development) : 3000);
82
+ } else if (pkg === "ui") {
83
+ port = bosConfig?.app?.ui
84
+ ? parsePort((bosConfig.app.ui as RemoteConfig).development)
85
+ : 3002;
86
+ } else if (pkg === "ui-ssr") {
87
+ // SSR build watcher doesn't need a port (build process, not server)
88
+ port = 0;
89
+ } else if (pkg === "api") {
90
+ port = bosConfig?.app?.api
91
+ ? parsePort((bosConfig.app.api as RemoteConfig).development)
92
+ : 3014;
93
+ } else {
94
+ port = 0;
95
+ }
96
+
97
+ // Set BUILD_TARGET for SSR build process
98
+ const processEnv =
99
+ pkg === "ui-ssr" ? { ...env, BUILD_TARGET: "server" } : env;
100
+
101
+ return { ...base, port, env: processEnv };
88
102
  };
89
103
 
90
104
  export interface ProcessCallbacks {
91
- onStatus: (name: string, status: ProcessStatus, message?: string) => void;
92
- onLog: (name: string, line: string, isError?: boolean) => void;
105
+ onStatus: (name: string, status: ProcessStatus, message?: string) => void;
106
+ onLog: (name: string, line: string, isError?: boolean) => void;
93
107
  }
94
108
 
95
109
  export interface ProcessHandle {
96
- name: string;
97
- pid: number | undefined;
98
- kill: () => Promise<void>;
99
- waitForReady: Effect.Effect<void>;
100
- waitForExit: Effect.Effect<unknown, unknown>;
110
+ name: string;
111
+ pid: number | undefined;
112
+ kill: () => Promise<void>;
113
+ waitForReady: Effect.Effect<void>;
114
+ waitForExit: Effect.Effect<unknown, unknown>;
101
115
  }
102
116
 
103
117
  const detectStatus = (
104
- line: string,
105
- config: DevProcess
118
+ line: string,
119
+ config: DevProcess,
106
120
  ): { status: ProcessStatus; isError: boolean } | null => {
107
- for (const pattern of config.errorPatterns) {
108
- if (pattern.test(line)) {
109
- return { status: "error", isError: true };
110
- }
111
- }
112
- for (const pattern of config.readyPatterns) {
113
- if (pattern.test(line)) {
114
- return { status: "ready", isError: false };
115
- }
116
- }
117
- return null;
121
+ for (const pattern of config.errorPatterns) {
122
+ if (pattern.test(line)) {
123
+ return { status: "error", isError: true };
124
+ }
125
+ }
126
+ for (const pattern of config.readyPatterns) {
127
+ if (pattern.test(line)) {
128
+ return { status: "ready", isError: false };
129
+ }
130
+ }
131
+ return null;
118
132
  };
119
133
 
120
134
  const killProcessTree = (pid: number) =>
121
- Effect.gen(function* () {
122
- const killSignal = (signal: NodeJS.Signals) =>
123
- Effect.try({
124
- try: () => {
125
- process.kill(-pid, signal);
126
- },
127
- catch: () => null,
128
- }).pipe(Effect.ignore);
129
-
130
- const killDirect = (signal: NodeJS.Signals) =>
131
- Effect.try({
132
- try: () => {
133
- process.kill(pid, signal);
134
- },
135
- catch: () => null,
136
- }).pipe(Effect.ignore);
137
-
138
- const isRunning = () =>
139
- Effect.try({
140
- try: () => {
141
- process.kill(pid, 0);
142
- return true;
143
- },
144
- catch: () => false,
145
- });
146
-
147
- yield* killSignal("SIGTERM");
148
- yield* killDirect("SIGTERM");
149
-
150
- yield* Effect.sleep("200 millis");
151
-
152
- const stillRunning = yield* isRunning();
153
- if (stillRunning) {
154
- yield* killSignal("SIGKILL");
155
- yield* killDirect("SIGKILL");
156
- yield* Effect.sleep("100 millis");
157
- }
158
- });
135
+ Effect.gen(function* () {
136
+ const killSignal = (signal: NodeJS.Signals) =>
137
+ Effect.try({
138
+ try: () => {
139
+ process.kill(-pid, signal);
140
+ },
141
+ catch: () => null,
142
+ }).pipe(Effect.ignore);
143
+
144
+ const killDirect = (signal: NodeJS.Signals) =>
145
+ Effect.try({
146
+ try: () => {
147
+ process.kill(pid, signal);
148
+ },
149
+ catch: () => null,
150
+ }).pipe(Effect.ignore);
151
+
152
+ const isRunning = () =>
153
+ Effect.try({
154
+ try: () => {
155
+ process.kill(pid, 0);
156
+ return true;
157
+ },
158
+ catch: () => false,
159
+ });
160
+
161
+ yield* killSignal("SIGTERM");
162
+ yield* killDirect("SIGTERM");
163
+
164
+ yield* Effect.sleep("200 millis");
165
+
166
+ const stillRunning = yield* isRunning();
167
+ if (stillRunning) {
168
+ yield* killSignal("SIGKILL");
169
+ yield* killDirect("SIGKILL");
170
+ yield* Effect.sleep("100 millis");
171
+ }
172
+ });
159
173
 
160
174
  export function buildRuntimeConfig(
161
- bosConfig: BosConfig,
162
- options: {
163
- uiSource: SourceMode;
164
- apiSource: SourceMode;
165
- hostUrl: string;
166
- proxy?: string;
167
- env?: "development" | "production";
168
- }
175
+ bosConfig: BosConfig,
176
+ options: {
177
+ uiSource: SourceMode;
178
+ apiSource: SourceMode;
179
+ hostUrl: string;
180
+ proxy?: string;
181
+ env?: "development" | "production";
182
+ },
169
183
  ): RuntimeConfig {
170
- const uiConfig = bosConfig.app.ui as RemoteConfig;
171
- const apiConfig = bosConfig.app.api as RemoteConfig;
172
-
173
- return {
174
- env: options.env ?? "development",
175
- account: bosConfig.account,
176
- hostUrl: options.hostUrl,
177
- shared: (bosConfig as { shared?: { ui?: Record<string, unknown> } }).shared as RuntimeConfig["shared"],
178
- ui: {
179
- name: uiConfig.name,
180
- url: options.uiSource === "remote" ? uiConfig.production : uiConfig.development,
181
- ssrUrl: options.uiSource === "remote" ? uiConfig.ssr : undefined,
182
- source: options.uiSource,
183
- },
184
- api: {
185
- name: apiConfig.name,
186
- url: options.apiSource === "remote" ? apiConfig.production : apiConfig.development,
187
- source: options.apiSource,
188
- proxy: options.proxy,
189
- variables: apiConfig.variables,
190
- secrets: apiConfig.secrets,
191
- },
192
- };
184
+ const uiConfig = bosConfig.app.ui as RemoteConfig;
185
+ const apiConfig = bosConfig.app.api as RemoteConfig;
186
+
187
+ return {
188
+ env: options.env ?? "development",
189
+ account: bosConfig.account,
190
+ hostUrl: options.hostUrl,
191
+ shared: (bosConfig as { shared?: { ui?: Record<string, unknown> } })
192
+ .shared as RuntimeConfig["shared"],
193
+ ui: {
194
+ name: uiConfig.name,
195
+ url:
196
+ options.uiSource === "remote"
197
+ ? uiConfig.production
198
+ : uiConfig.development,
199
+ entry: `${
200
+ options.uiSource === "remote"
201
+ ? uiConfig.production
202
+ : uiConfig.development
203
+ }/remoteEntry.js`,
204
+ source: options.uiSource,
205
+ },
206
+ api: {
207
+ name: apiConfig.name,
208
+ url:
209
+ options.apiSource === "remote"
210
+ ? apiConfig.production
211
+ : apiConfig.development,
212
+ entry: `${
213
+ options.apiSource === "remote"
214
+ ? apiConfig.production
215
+ : apiConfig.development
216
+ }/remoteEntry.js`,
217
+ source: options.apiSource,
218
+ proxy: options.proxy,
219
+ variables: apiConfig.variables,
220
+ secrets: apiConfig.secrets,
221
+ },
222
+ };
193
223
  }
194
224
 
195
225
  export const spawnDevProcess = (
196
- config: DevProcess,
197
- callbacks: ProcessCallbacks,
198
- runtimeConfig?: RuntimeConfig
226
+ config: DevProcess,
227
+ callbacks: ProcessCallbacks,
228
+ runtimeConfig?: RuntimeConfig,
199
229
  ) =>
200
- Effect.gen(function* () {
201
- const configDir = getConfigDir();
202
- const fullCwd = `${configDir}/${config.cwd}`;
203
- const readyDeferred = yield* Deferred.make<void>();
204
- const statusRef = yield* Ref.make<ProcessStatus>("starting");
205
-
206
- callbacks.onStatus(config.name, "starting");
207
-
208
- const envVars: Record<string, string> = {
209
- ...process.env as Record<string, string>,
210
- ...config.env,
211
- FORCE_COLOR: "1",
212
- ...(config.port > 0 ? { PORT: String(config.port) } : {}),
213
- };
214
-
215
- if (runtimeConfig && config.name === "host") {
216
- envVars.BOS_RUNTIME_CONFIG = JSON.stringify(runtimeConfig);
217
- }
218
-
219
- const cmd = Command.make(config.command, ...config.args).pipe(
220
- Command.workingDirectory(fullCwd),
221
- Command.env(envVars)
222
- );
223
-
224
- const proc = yield* Command.start(cmd);
225
-
226
- const handleLine = (line: string, isStderr: boolean) =>
227
- Effect.gen(function* () {
228
- if (!line.trim()) return;
229
-
230
- callbacks.onLog(config.name, line, isStderr);
231
-
232
- const currentStatus = yield* Ref.get(statusRef);
233
- if (currentStatus === "ready") return;
234
-
235
- const detected = detectStatus(line, config);
236
- if (detected) {
237
- yield* Ref.set(statusRef, detected.status);
238
- callbacks.onStatus(config.name, detected.status);
239
- if (detected.status === "ready") {
240
- yield* Deferred.succeed(readyDeferred, undefined);
241
- }
242
- }
243
- });
244
-
245
- const stdoutFiber = yield* Effect.fork(
246
- proc.stdout.pipe(
247
- Stream.decodeText(),
248
- Stream.splitLines,
249
- Stream.runForEach((line) => handleLine(line, false))
250
- )
251
- );
252
-
253
- const stderrFiber = yield* Effect.fork(
254
- proc.stderr.pipe(
255
- Stream.decodeText(),
256
- Stream.splitLines,
257
- Stream.runForEach((line) => handleLine(line, true))
258
- )
259
- );
260
-
261
- const handle: ProcessHandle = {
262
- name: config.name,
263
- pid: proc.pid,
264
- kill: async () => {
265
- const pid = proc.pid;
266
- if (pid) {
267
- await Effect.runPromise(killProcessTree(pid));
268
- } else {
269
- proc.kill("SIGTERM");
270
- await new Promise((r) => setTimeout(r, 100));
271
- try {
272
- proc.kill("SIGKILL");
273
- } catch { }
274
- }
275
- },
276
- waitForReady: Deferred.await(readyDeferred),
277
- waitForExit: Effect.gen(function* () {
278
- yield* Fiber.joinAll([stdoutFiber, stderrFiber]);
279
- return yield* proc.exitCode;
280
- }),
281
- };
282
-
283
- return handle;
284
- });
230
+ Effect.gen(function* () {
231
+ let configDir: string;
232
+ try {
233
+ configDir = getProjectRoot();
234
+ } catch {
235
+ configDir = process.cwd();
236
+ }
237
+ const fullCwd = `${configDir}/${config.cwd}`;
238
+ const readyDeferred = yield* Deferred.make<void>();
239
+ const statusRef = yield* Ref.make<ProcessStatus>("starting");
240
+
241
+ callbacks.onStatus(config.name, "starting");
242
+
243
+ const envVars: Record<string, string> = {
244
+ ...(process.env as Record<string, string>),
245
+ ...config.env,
246
+ FORCE_COLOR: "1",
247
+ ...(config.port > 0 ? { PORT: String(config.port) } : {}),
248
+ };
249
+
250
+ if (runtimeConfig && config.name === "host") {
251
+ envVars.BOS_RUNTIME_CONFIG = JSON.stringify(runtimeConfig);
252
+ }
253
+
254
+ // Spawn process using Bun.spawn (replaces @effect/platform Command)
255
+ const proc = Bun.spawn({
256
+ cmd: [config.command, ...config.args],
257
+ cwd: fullCwd,
258
+ env: envVars,
259
+ stdio: ["inherit", "pipe", "pipe"],
260
+ });
261
+
262
+ const handleLine = (line: string, isStderr: boolean) =>
263
+ Effect.gen(function* () {
264
+ if (!line.trim()) return;
265
+
266
+ callbacks.onLog(config.name, line, isStderr);
267
+
268
+ const currentStatus = yield* Ref.get(statusRef);
269
+ if (currentStatus === "ready") return;
270
+
271
+ const detected = detectStatus(line, config);
272
+ if (detected) {
273
+ yield* Ref.set(statusRef, detected.status);
274
+ callbacks.onStatus(config.name, detected.status);
275
+ if (detected.status === "ready") {
276
+ yield* Deferred.succeed(readyDeferred, undefined);
277
+ }
278
+ }
279
+ });
280
+
281
+ // Read stdout and stderr using raw stream readers (replaces Effect Stream)
282
+ const decoder = new TextDecoder();
283
+
284
+ const stdoutFiber = yield* Effect.fork(
285
+ Effect.async<void>((resume) => {
286
+ if (!proc.stdout) {
287
+ resume(Effect.void);
288
+ return;
289
+ }
290
+ const reader = proc.stdout.getReader();
291
+ let buffer = "";
292
+
293
+ const pump = (): Promise<void> =>
294
+ reader.read().then(({ done, value }) => {
295
+ if (done) {
296
+ if (buffer) {
297
+ Effect.runSync(handleLine(buffer, false));
298
+ }
299
+ return;
300
+ }
301
+ buffer += decoder.decode(value, { stream: true });
302
+ const lines = buffer.split("\n");
303
+ buffer = lines.pop() ?? "";
304
+ for (const line of lines) {
305
+ Effect.runSync(handleLine(line, false));
306
+ }
307
+ return pump();
308
+ });
309
+
310
+ pump().then(() => resume(Effect.void));
311
+ }),
312
+ );
313
+
314
+ const stderrFiber = yield* Effect.fork(
315
+ Effect.async<void>((resume) => {
316
+ if (!proc.stderr) {
317
+ resume(Effect.void);
318
+ return;
319
+ }
320
+ const reader = proc.stderr.getReader();
321
+ let buffer = "";
322
+
323
+ const pump = (): Promise<void> =>
324
+ reader.read().then(({ done, value }) => {
325
+ if (done) {
326
+ if (buffer) {
327
+ Effect.runSync(handleLine(buffer, true));
328
+ }
329
+ return;
330
+ }
331
+ buffer += decoder.decode(value, { stream: true });
332
+ const lines = buffer.split("\n");
333
+ buffer = lines.pop() ?? "";
334
+ for (const line of lines) {
335
+ Effect.runSync(handleLine(line, true));
336
+ }
337
+ return pump();
338
+ });
339
+
340
+ pump().then(() => resume(Effect.void));
341
+ }),
342
+ );
343
+
344
+ const handle: ProcessHandle = {
345
+ name: config.name,
346
+ pid: proc.pid,
347
+ kill: async () => {
348
+ const pid = proc.pid;
349
+ if (pid) {
350
+ await Effect.runPromise(killProcessTree(pid));
351
+ } else {
352
+ proc.kill("SIGTERM");
353
+ await new Promise((r) => setTimeout(r, 100));
354
+ try {
355
+ proc.kill("SIGKILL");
356
+ } catch {}
357
+ }
358
+ },
359
+ waitForReady: Deferred.await(readyDeferred),
360
+ waitForExit: Effect.gen(function* () {
361
+ yield* Fiber.joinAll([stdoutFiber, stderrFiber]);
362
+ return yield* Effect.promise(() => proc.exited);
363
+ }),
364
+ };
365
+
366
+ return handle;
367
+ });
285
368
 
286
369
  interface ServerHandle {
287
- ready: Promise<void>;
288
- shutdown: () => Promise<void>;
370
+ ready: Promise<void>;
371
+ shutdown: () => Promise<void>;
289
372
  }
290
373
 
291
374
  interface ServerInput {
292
- config: RuntimeConfig;
375
+ config: RuntimeConfig;
293
376
  }
294
377
 
295
378
  const patchConsole = (
296
- name: string,
297
- callbacks: ProcessCallbacks
379
+ name: string,
380
+ callbacks: ProcessCallbacks,
298
381
  ): (() => void) => {
299
- const originalLog = console.log;
300
- const originalError = console.error;
301
- const originalWarn = console.warn;
302
- const originalInfo = console.info;
303
-
304
- const formatArgs = (args: unknown[]): string => {
305
- return args
306
- .map((arg) =>
307
- typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg)
308
- )
309
- .join(" ");
310
- };
311
-
312
- console.log = (...args: unknown[]) => {
313
- callbacks.onLog(name, formatArgs(args), false);
314
- };
315
-
316
- console.error = (...args: unknown[]) => {
317
- callbacks.onLog(name, formatArgs(args), true);
318
- };
319
-
320
- console.warn = (...args: unknown[]) => {
321
- callbacks.onLog(name, formatArgs(args), false);
322
- };
323
-
324
- console.info = (...args: unknown[]) => {
325
- callbacks.onLog(name, formatArgs(args), false);
326
- };
327
-
328
- return () => {
329
- console.log = originalLog;
330
- console.error = originalError;
331
- console.warn = originalWarn;
332
- console.info = originalInfo;
333
- };
382
+ const originalLog = console.log;
383
+ const originalError = console.error;
384
+ const originalWarn = console.warn;
385
+ const originalInfo = console.info;
386
+
387
+ const formatArgs = (args: unknown[]): string => {
388
+ return args
389
+ .map((arg) =>
390
+ typeof arg === "object" ? JSON.stringify(arg, null, 2) : String(arg),
391
+ )
392
+ .join(" ");
393
+ };
394
+
395
+ console.log = (...args: unknown[]) => {
396
+ callbacks.onLog(name, formatArgs(args), false);
397
+ };
398
+
399
+ console.error = (...args: unknown[]) => {
400
+ callbacks.onLog(name, formatArgs(args), true);
401
+ };
402
+
403
+ console.warn = (...args: unknown[]) => {
404
+ callbacks.onLog(name, formatArgs(args), false);
405
+ };
406
+
407
+ console.info = (...args: unknown[]) => {
408
+ callbacks.onLog(name, formatArgs(args), false);
409
+ };
410
+
411
+ return () => {
412
+ console.log = originalLog;
413
+ console.error = originalError;
414
+ console.warn = originalWarn;
415
+ console.info = originalInfo;
416
+ };
334
417
  };
335
418
 
336
419
  export const spawnRemoteHost = (
337
- config: DevProcess,
338
- callbacks: ProcessCallbacks,
339
- runtimeConfig: RuntimeConfig
420
+ config: DevProcess,
421
+ callbacks: ProcessCallbacks,
422
+ runtimeConfig: RuntimeConfig,
340
423
  ) =>
341
- Effect.gen(function* () {
342
- const remoteUrl = config.env?.HOST_REMOTE_URL;
343
-
344
- if (!remoteUrl) {
345
- return yield* Effect.fail(new Error("HOST_REMOTE_URL not provided for remote host"));
346
- }
347
-
348
- if (config.env) {
349
- for (const [key, value] of Object.entries(config.env)) {
350
- process.env[key] = value;
351
- }
352
- }
353
-
354
- callbacks.onStatus(config.name, "starting");
355
-
356
- callbacks.onLog(config.name, `Remote: ${remoteUrl}`);
357
-
358
- const restoreConsole = patchConsole(config.name, callbacks);
359
-
360
- callbacks.onLog(config.name, "Loading Module Federation runtime...");
361
-
362
- const mfRuntime = yield* Effect.tryPromise({
363
- try: () => import("@module-federation/enhanced/runtime"),
364
- catch: (e) => new Error(`Failed to load MF runtime: ${e}`),
365
- });
366
-
367
- const mfCore = yield* Effect.tryPromise({
368
- try: () => import("@module-federation/runtime-core"),
369
- catch: (e) => new Error(`Failed to load MF core: ${e}`),
370
- });
371
-
372
- let mf = mfRuntime.getInstance();
373
- if (!mf) {
374
- mf = mfRuntime.createInstance({ name: "cli-host", remotes: [] });
375
- mfCore.setGlobalFederationInstance(mf);
376
- }
377
-
378
- const remoteEntryUrl = remoteUrl.endsWith("/remoteEntry.js")
379
- ? remoteUrl
380
- : `${remoteUrl}/remoteEntry.js`;
381
-
382
- mf.registerRemotes([{ name: "host", entry: remoteEntryUrl }]);
383
-
384
- callbacks.onLog(config.name, `Loading host from ${remoteEntryUrl}...`);
385
-
386
- const hostModule = yield* Effect.tryPromise({
387
- try: () => mf.loadRemote<{ runServer: (input: ServerInput) => ServerHandle }>("host/Server"),
388
- catch: (e) => new Error(`Failed to load host module: ${e}`),
389
- });
390
-
391
- if (!hostModule?.runServer) {
392
- return yield* Effect.fail(new Error("Host module does not export runServer function"));
393
- }
394
-
395
- callbacks.onLog(config.name, "Starting server...");
396
- const serverHandle = hostModule.runServer({ config: runtimeConfig });
397
-
398
- yield* Effect.tryPromise({
399
- try: () => serverHandle.ready,
400
- catch: (e) => new Error(`Server failed to start: ${e}`),
401
- });
402
-
403
- callbacks.onStatus(config.name, "ready");
404
-
405
- const handle: ProcessHandle = {
406
- name: config.name,
407
- pid: process.pid,
408
- kill: async () => {
409
- callbacks.onLog(config.name, "Shutting down remote host...");
410
- restoreConsole();
411
- await serverHandle.shutdown();
412
- },
413
- waitForReady: Effect.void,
414
- waitForExit: Effect.never,
415
- };
416
-
417
- return handle;
418
- });
424
+ Effect.gen(function* () {
425
+ const remoteUrl = config.env?.HOST_REMOTE_URL;
426
+
427
+ if (!remoteUrl) {
428
+ return yield* Effect.fail(
429
+ new Error("HOST_REMOTE_URL not provided for remote host"),
430
+ );
431
+ }
432
+
433
+ if (config.env) {
434
+ for (const [key, value] of Object.entries(config.env)) {
435
+ process.env[key] = value;
436
+ }
437
+ }
438
+
439
+ callbacks.onStatus(config.name, "starting");
440
+
441
+ callbacks.onLog(config.name, `Remote: ${remoteUrl}`);
442
+
443
+ const restoreConsole = patchConsole(config.name, callbacks);
444
+
445
+ callbacks.onLog(config.name, "Loading Module Federation runtime...");
446
+
447
+ const mfRuntime = yield* Effect.tryPromise({
448
+ try: () => import("@module-federation/enhanced/runtime"),
449
+ catch: (e) => new Error(`Failed to load MF runtime: ${e}`),
450
+ });
451
+
452
+ const mfCore = yield* Effect.tryPromise({
453
+ try: () => import("@module-federation/runtime-core"),
454
+ catch: (e) => new Error(`Failed to load MF core: ${e}`),
455
+ });
456
+
457
+ let mf = mfRuntime.getInstance();
458
+ if (!mf) {
459
+ mf = mfRuntime.createInstance({ name: "cli-host", remotes: [] });
460
+ mfCore.setGlobalFederationInstance(mf);
461
+ }
462
+
463
+ const remoteEntryUrl = remoteUrl.endsWith("/remoteEntry.js")
464
+ ? remoteUrl
465
+ : `${remoteUrl}/remoteEntry.js`;
466
+
467
+ mf.registerRemotes([{ name: "host", entry: remoteEntryUrl }]);
468
+
469
+ callbacks.onLog(config.name, `Loading host from ${remoteEntryUrl}...`);
470
+
471
+ const hostModule = yield* Effect.tryPromise({
472
+ try: () =>
473
+ mf.loadRemote<{ runServer: (input: ServerInput) => ServerHandle }>(
474
+ "host/Server",
475
+ ),
476
+ catch: (e) => new Error(`Failed to load host module: ${e}`),
477
+ });
478
+
479
+ if (!hostModule?.runServer) {
480
+ return yield* Effect.fail(
481
+ new Error("Host module does not export runServer function"),
482
+ );
483
+ }
484
+
485
+ callbacks.onLog(config.name, "Starting server...");
486
+ const serverHandle = hostModule.runServer({ config: runtimeConfig });
487
+
488
+ yield* Effect.tryPromise({
489
+ try: () => serverHandle.ready,
490
+ catch: (e) => new Error(`Server failed to start: ${e}`),
491
+ });
492
+
493
+ callbacks.onStatus(config.name, "ready");
494
+
495
+ const handle: ProcessHandle = {
496
+ name: config.name,
497
+ pid: process.pid,
498
+ kill: async () => {
499
+ callbacks.onLog(config.name, "Shutting down remote host...");
500
+ restoreConsole();
501
+ await serverHandle.shutdown();
502
+ },
503
+ waitForReady: Effect.void,
504
+ waitForExit: Effect.never,
505
+ };
506
+
507
+ return handle;
508
+ });
419
509
 
420
510
  export const makeDevProcess = (
421
- pkg: string,
422
- env: Record<string, string> | undefined,
423
- callbacks: ProcessCallbacks,
424
- portOverride?: number,
425
- bosConfig?: BosConfig
511
+ pkg: string,
512
+ env: Record<string, string> | undefined,
513
+ callbacks: ProcessCallbacks,
514
+ portOverride?: number,
515
+ bosConfig?: BosConfig,
426
516
  ) =>
427
- Effect.gen(function* () {
428
- const config = getProcessConfig(pkg, env, portOverride);
429
- if (!config) {
430
- return yield* Effect.fail(new Error(`Unknown package: ${pkg}`));
431
- }
432
-
433
- if (pkg === "host" && bosConfig) {
434
- const uiSource = (env?.UI_SOURCE as SourceMode) ?? "local";
435
- const apiSource = (env?.API_SOURCE as SourceMode) ?? "local";
436
- const apiProxy = env?.API_PROXY;
437
-
438
- let hostUrl = `http://localhost:${config.port}`;
439
- if (process.env.HOST_URL) {
440
- hostUrl = process.env.HOST_URL;
441
- }
442
-
443
- const runtimeConfig = buildRuntimeConfig(bosConfig, {
444
- uiSource,
445
- apiSource,
446
- hostUrl,
447
- proxy: apiProxy,
448
- env: "development",
449
- });
450
-
451
- if (env?.HOST_SOURCE === "remote") {
452
- return yield* spawnRemoteHost(config, callbacks, runtimeConfig);
453
- }
454
-
455
- return yield* spawnDevProcess(config, callbacks, runtimeConfig);
456
- }
457
-
458
- return yield* spawnDevProcess(config, callbacks);
459
- });
517
+ Effect.gen(function* () {
518
+ const config = getProcessConfig(pkg, env, portOverride);
519
+ if (!config) {
520
+ return yield* Effect.fail(new Error(`Unknown package: ${pkg}`));
521
+ }
522
+
523
+ if (pkg === "host" && bosConfig) {
524
+ const uiSource = (env?.UI_SOURCE as SourceMode) ?? "local";
525
+ const apiSource = (env?.API_SOURCE as SourceMode) ?? "local";
526
+ const apiProxy = env?.API_PROXY;
527
+
528
+ let hostUrl = `http://localhost:${config.port}`;
529
+ if (process.env.HOST_URL) {
530
+ hostUrl = process.env.HOST_URL;
531
+ }
532
+
533
+ const runtimeConfig = buildRuntimeConfig(bosConfig, {
534
+ uiSource,
535
+ apiSource,
536
+ hostUrl,
537
+ proxy: apiProxy,
538
+ env: "development",
539
+ });
540
+
541
+ if (env?.HOST_SOURCE === "remote") {
542
+ return yield* spawnRemoteHost(config, callbacks, runtimeConfig);
543
+ }
544
+
545
+ return yield* spawnDevProcess(config, callbacks, runtimeConfig);
546
+ }
547
+
548
+ return yield* spawnDevProcess(config, callbacks);
549
+ });