@fluojs/cli 1.0.4 → 1.0.5
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.ko.md +23 -0
- package/README.md +23 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/commands/scripts.d.ts +2 -0
- package/dist/commands/scripts.d.ts.map +1 -1
- package/dist/commands/scripts.js +150 -34
- package/dist/dev-runner/node-restart-runner.d.ts.map +1 -1
- package/dist/dev-runner/node-restart-runner.js +72 -1
- package/dist/studio/runtime-config.d.ts +20 -0
- package/dist/studio/runtime-config.d.ts.map +1 -0
- package/dist/studio/runtime-config.js +51 -0
- package/dist/studio/sidecar.d.ts +35 -0
- package/dist/studio/sidecar.d.ts.map +1 -0
- package/dist/studio/sidecar.js +401 -0
- package/package.json +3 -3
package/README.ko.md
CHANGED
|
@@ -184,6 +184,29 @@ fluo start --dry-run
|
|
|
184
184
|
|
|
185
185
|
`fluo dev --dry-run`은 watch boundary도 함께 표시합니다. 생성된 Node 프로젝트는 기본적으로 `Watch mode: fluo-restart`를 보여 주며, Node의 `--raw-watch` 또는 `FLUO_DEV_RAW_WATCH=1`은 `Watch mode: native-watch`를 보여 줍니다. Bun/Deno/Workers 프로젝트는 기본적으로 `Watch mode: runtime-native-watch`를 보여 주며, `--runner fluo` 또는 `FLUO_DEV_RUNNER=fluo`를 사용하면 `Watch mode: fluo-restart`로 fluo 소유 restart runner가 복원됩니다.
|
|
186
186
|
|
|
187
|
+
### Runtime-connected Studio devtool
|
|
188
|
+
|
|
189
|
+
실행 중인 앱에 local React Studio devtool을 붙이고 싶을 때는 static HTML/JSON을 먼저 내보내지 말고 `fluo dev --studio`를 사용합니다.
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
fluo dev --studio
|
|
193
|
+
fluo dev --studio --studio-port 51234
|
|
194
|
+
fluo dev --studio --dry-run
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
CLI는 local Studio sidecar를 시작하고, tokenized URL을 출력하며, restart lifecycle event를 sidecar로 계속 전달하고, 앱이 `@fluojs/runtime`을 import하기 전에 명시적인 Studio config를 Node 앱 child에 주입합니다. Optional package인 `@fluojs/studio`가 설치되어 있으면 sidecar는 패키징된 `@fluojs/studio/viewer` React app을 제공합니다. Runtime package source는 `process.env`를 직접 읽지 않으며, CLI가 주입한 Studio config가 있을 때만 live graph/routes/request/timing/diagnostic event를 전송합니다.
|
|
198
|
+
|
|
199
|
+
보안 기본값은 local-only입니다. Sidecar는 `127.0.0.1`에 bind되고, runtime ingestion 및 browser state/SSE API는 generated token을 요구하며, CORS는 기본적으로 활성화하지 않고, request body는 기본적으로 수집하지 않습니다.
|
|
200
|
+
|
|
201
|
+
MVP runtime support는 명시적으로 제한됩니다.
|
|
202
|
+
|
|
203
|
+
| Runtime target | `fluo dev --studio` status |
|
|
204
|
+
| --- | --- |
|
|
205
|
+
| Node dev runner | Full support target입니다. |
|
|
206
|
+
| Bun | 이번 MVP에서는 활성화하지 않습니다. Dedicated bridge를 구현하고 검증하기 전까지 `fluo dev --studio`는 Bun 프로젝트를 거부합니다. |
|
|
207
|
+
| Deno | 이번 MVP에서는 활성화하지 않습니다. Dedicated bridge를 구현하고 검증하기 전까지 `fluo dev --studio`는 Deno 프로젝트를 거부합니다. |
|
|
208
|
+
| Cloudflare Workers | worker bridge를 추가하고 테스트하지 않는 한 이번 MVP에서는 unsupported입니다. |
|
|
209
|
+
|
|
187
210
|
CLI process boundary를 조정해야 할 때는 런타임 앱 로깅이 아니라 reporter flag를 사용하세요:
|
|
188
211
|
|
|
189
212
|
```bash
|
package/README.md
CHANGED
|
@@ -184,6 +184,29 @@ fluo start --dry-run
|
|
|
184
184
|
|
|
185
185
|
`fluo dev --dry-run` also reports the watch boundary. Generated Node projects show `Watch mode: fluo-restart` by default, Node `--raw-watch` and `FLUO_DEV_RAW_WATCH=1` show `Watch mode: native-watch`, and Bun/Deno/Workers projects show `Watch mode: runtime-native-watch` by default. Use `--runner fluo` or `FLUO_DEV_RUNNER=fluo` in Bun/Deno/Workers projects to show `Watch mode: fluo-restart` and restore the fluo-owned restart runner.
|
|
186
186
|
|
|
187
|
+
### Runtime-connected Studio devtool
|
|
188
|
+
|
|
189
|
+
Use `fluo dev --studio` when you want the local React Studio devtool attached to the running app instead of exporting static HTML/JSON first:
|
|
190
|
+
|
|
191
|
+
```bash
|
|
192
|
+
fluo dev --studio
|
|
193
|
+
fluo dev --studio --studio-port 51234
|
|
194
|
+
fluo dev --studio --dry-run
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The CLI starts a local Studio sidecar, prints a tokenized URL, keeps restart lifecycle events flowing through the sidecar, and injects an explicit Studio config into the Node app child before the app imports `@fluojs/runtime`. The sidecar serves the packaged `@fluojs/studio/viewer` React app when that optional package is installed. Runtime package source never reads `process.env` directly; it publishes live graph/routes/request/timing/diagnostic events only when CLI-injected Studio config is present.
|
|
198
|
+
|
|
199
|
+
Security defaults are local-only: the sidecar binds `127.0.0.1`, runtime ingestion and browser state/SSE APIs require generated tokens, CORS is not enabled by default, and request bodies are not captured by default.
|
|
200
|
+
|
|
201
|
+
Runtime support for the MVP is explicit:
|
|
202
|
+
|
|
203
|
+
| Runtime target | `fluo dev --studio` status |
|
|
204
|
+
| --- | --- |
|
|
205
|
+
| Node dev runner | Full support target. |
|
|
206
|
+
| Bun | Not enabled for this MVP; `fluo dev --studio` rejects Bun projects until a dedicated bridge is implemented and verified. |
|
|
207
|
+
| Deno | Not enabled for this MVP; `fluo dev --studio` rejects Deno projects until a dedicated bridge is implemented and verified. |
|
|
208
|
+
| Cloudflare Workers | Unsupported for this MVP unless a worker bridge is added and tested. |
|
|
209
|
+
|
|
187
210
|
Use reporter flags when you need to tune the CLI process boundary rather than runtime app logging:
|
|
188
211
|
|
|
189
212
|
```bash
|
package/dist/cli.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type InspectCommandRuntimeOptions } from './commands/inspect.js';
|
|
2
2
|
import { type NewCommandRuntimeOptions } from './commands/new.js';
|
|
3
|
+
import type { startStudioSidecar } from './studio/sidecar.js';
|
|
3
4
|
import { type CliUpdateCheckRuntimeOptions } from './update-check.js';
|
|
4
5
|
type CliStream = {
|
|
5
6
|
isTTY?: boolean;
|
|
@@ -23,6 +24,7 @@ export interface CliRuntimeOptions {
|
|
|
23
24
|
stdio: 'inherit' | 'pipe';
|
|
24
25
|
stdout?: CliStream;
|
|
25
26
|
}) => Promise<number>;
|
|
27
|
+
startStudioSidecar?: typeof startStudioSidecar;
|
|
26
28
|
stderr?: CliStream;
|
|
27
29
|
stdin?: CliReadableStream;
|
|
28
30
|
stdout?: CliStream;
|
package/dist/cli.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,4BAA4B,EAAmC,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,KAAK,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,KAAK,4BAA4B,EAAmC,MAAM,uBAAuB,CAAC;AAE3G,OAAO,EAAE,KAAK,wBAAwB,EAA2B,MAAM,mBAAmB,CAAC;AAI3F,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAI9D,OAAO,EAAE,KAAK,4BAA4B,EAA6C,MAAM,mBAAmB,CAAC;AAEjH,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,iBAAiB,GAAG;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,aAAa,CAAC,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC,CAAC;IACrF,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAC;QAAC,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,SAAS,CAAA;KAAE,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACzL,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IAC/C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,WAAW,CAAC,EAAE,KAAK,GAAG,4BAA4B,CAAC;CACpD;AA4ZD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAsB,MAAM,CAC1B,IAAI,WAAwB,EAC5B,OAAO,GAAE,iBAAiB,GAAG,wBAAwB,GAAG,4BAAiC,GACxF,OAAO,CAAC,MAAM,CAAC,CAuNjB"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { startStudioSidecar } from '../studio/sidecar.js';
|
|
1
2
|
type CliStream = {
|
|
2
3
|
isTTY?: boolean;
|
|
3
4
|
write(message: string): unknown;
|
|
@@ -14,6 +15,7 @@ type ScriptRuntimeOptions = {
|
|
|
14
15
|
cwd?: string;
|
|
15
16
|
env?: NodeJS.ProcessEnv;
|
|
16
17
|
spawnCommand?: (command: string, args: string[], options: SpawnCommandOptions) => Promise<number>;
|
|
18
|
+
startStudioSidecar?: typeof startStudioSidecar;
|
|
17
19
|
stderr?: CliStream;
|
|
18
20
|
stdout?: CliStream;
|
|
19
21
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/commands/scripts.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"scripts.d.ts","sourceRoot":"","sources":["../../src/commands/scripts.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,kBAAkB,EAAiD,MAAM,sBAAsB,CAAC;AAGzG,KAAK,SAAS,GAAG;IACf,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAMF,KAAK,mBAAmB,GAAG;IACzB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,EAAE,SAAS,GAAG,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClG,kBAAkB,CAAC,EAAE,OAAO,kBAAkB,CAAC;IAC/C,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAGF,KAAK,aAAa,GAAG,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;AAulB/C;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAqB1D;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsFlI"}
|
package/dist/commands/scripts.js
CHANGED
|
@@ -2,6 +2,8 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { delimiter, dirname, join, resolve } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { createStudioDevtoolsNodeImport } from '../studio/runtime-config.js';
|
|
6
|
+
import { startStudioSidecar } from '../studio/sidecar.js';
|
|
5
7
|
import { SUPPORTED_PACKAGE_MANAGERS } from './package-manager.js';
|
|
6
8
|
const EMPTY_ENV = {};
|
|
7
9
|
const FAILURE_STDOUT_BUFFER_LIMIT = 16_384;
|
|
@@ -241,19 +243,18 @@ async function runProjectRunnerSteps(steps, runtime, options) {
|
|
|
241
243
|
}
|
|
242
244
|
return 0;
|
|
243
245
|
}
|
|
244
|
-
function
|
|
245
|
-
if (env[PRETTY_TTY_COLOR_ENV] !== '1') {
|
|
246
|
-
return steps;
|
|
247
|
-
}
|
|
246
|
+
function withPipedAppBootstrapImports(steps, env) {
|
|
248
247
|
return steps.map(step => {
|
|
249
248
|
const preserveColorTtyImport = getPreserveColorTtyImport();
|
|
250
|
-
|
|
249
|
+
const colorImport = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--import', preserveColorTtyImport] : [];
|
|
250
|
+
const studioDevtoolsImport = step.mode === 'fluo-restart' ? [] : createStudioDevtoolsNodeImport(env);
|
|
251
|
+
if (step.command === 'node') {
|
|
251
252
|
return {
|
|
252
253
|
...step,
|
|
253
|
-
args: [
|
|
254
|
+
args: [...colorImport, ...studioDevtoolsImport, ...step.args]
|
|
254
255
|
};
|
|
255
256
|
}
|
|
256
|
-
if (step.command === 'bun' && (step.mode === 'runtime-native-watch' || step.args[0] === 'dist/main.js')) {
|
|
257
|
+
if (env[PRETTY_TTY_COLOR_ENV] === '1' && step.command === 'bun' && (step.mode === 'runtime-native-watch' || step.args[0] === 'dist/main.js')) {
|
|
257
258
|
return {
|
|
258
259
|
...step,
|
|
259
260
|
args: ['--preload', preserveColorTtyImport, ...step.args]
|
|
@@ -268,6 +269,8 @@ function parseScriptArgs(argv) {
|
|
|
268
269
|
let packageManager;
|
|
269
270
|
let rawWatch = false;
|
|
270
271
|
let reporter = 'auto';
|
|
272
|
+
let studio = false;
|
|
273
|
+
let studioPort;
|
|
271
274
|
let verbose = false;
|
|
272
275
|
const passThrough = [];
|
|
273
276
|
for (let index = 0; index < argv.length; index += 1) {
|
|
@@ -308,6 +311,24 @@ function parseScriptArgs(argv) {
|
|
|
308
311
|
index += 1;
|
|
309
312
|
continue;
|
|
310
313
|
}
|
|
314
|
+
if (arg === '--studio') {
|
|
315
|
+
studio = true;
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (arg === '--studio-port') {
|
|
319
|
+
const value = argv[index + 1];
|
|
320
|
+
if (!value || value.startsWith('-')) {
|
|
321
|
+
throw new Error('Expected --studio-port to have a value.');
|
|
322
|
+
}
|
|
323
|
+
const parsedPort = Number(value);
|
|
324
|
+
if (!Number.isInteger(parsedPort) || parsedPort < 0 || parsedPort > 65_535) {
|
|
325
|
+
throw new Error('Invalid --studio-port value. Use a TCP port between 0 and 65535.');
|
|
326
|
+
}
|
|
327
|
+
studio = true;
|
|
328
|
+
studioPort = parsedPort;
|
|
329
|
+
index += 1;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
311
332
|
if (arg === '--package-manager') {
|
|
312
333
|
const value = argv[index + 1];
|
|
313
334
|
if (!value || value.startsWith('-')) {
|
|
@@ -333,6 +354,8 @@ function parseScriptArgs(argv) {
|
|
|
333
354
|
passThrough,
|
|
334
355
|
rawWatch,
|
|
335
356
|
reporter,
|
|
357
|
+
studio,
|
|
358
|
+
studioPort,
|
|
336
359
|
verbose
|
|
337
360
|
};
|
|
338
361
|
}
|
|
@@ -358,6 +381,37 @@ function resolveDevRunnerPreference(parsed, env, runtime) {
|
|
|
358
381
|
}
|
|
359
382
|
throw new Error(`Invalid FLUO_DEV_RUNNER value "${configured}". Use one of: fluo, native.`);
|
|
360
383
|
}
|
|
384
|
+
function projectRuntimeToStudioRuntime(runtime) {
|
|
385
|
+
if (runtime === 'bun' || runtime === 'deno' || runtime === 'node') {
|
|
386
|
+
return runtime;
|
|
387
|
+
}
|
|
388
|
+
return 'unknown';
|
|
389
|
+
}
|
|
390
|
+
function projectDisplayName(project) {
|
|
391
|
+
return typeof project.manifest.name === 'string' && project.manifest.name.length > 0 ? project.manifest.name : project.directory.split(/[\\/]/).filter(Boolean).at(-1) ?? 'fluo-app';
|
|
392
|
+
}
|
|
393
|
+
function assertStudioSupport(command, studio, projectRuntime, _devRunner) {
|
|
394
|
+
if (!studio) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
if (command !== 'dev') {
|
|
398
|
+
throw new Error('--studio is only supported for fluo dev.');
|
|
399
|
+
}
|
|
400
|
+
if (projectRuntime !== 'node') {
|
|
401
|
+
throw new Error(`fluo dev --studio currently supports Node dev runner projects only. ${projectRuntime} Studio support remains experimental until a dedicated bridge is implemented and verified.`);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function withStudioDryRunEnv(env, project, projectRuntime) {
|
|
405
|
+
return {
|
|
406
|
+
...env,
|
|
407
|
+
FLUO_STUDIO: '1',
|
|
408
|
+
FLUO_STUDIO_APP_ID: projectDisplayName(project),
|
|
409
|
+
FLUO_STUDIO_EPOCH: '<generated-at-runtime>',
|
|
410
|
+
FLUO_STUDIO_RUNTIME: projectRuntimeToStudioRuntime(projectRuntime),
|
|
411
|
+
FLUO_STUDIO_TOKEN: '<generated-at-runtime>',
|
|
412
|
+
FLUO_STUDIO_URL: 'http://127.0.0.1:<auto>'
|
|
413
|
+
};
|
|
414
|
+
}
|
|
361
415
|
function renderStep(step) {
|
|
362
416
|
return `${step.command} ${step.args.join(' ')}`.trim();
|
|
363
417
|
}
|
|
@@ -474,6 +528,57 @@ function createReporterStreams(mode, verbose, stdout, stderr) {
|
|
|
474
528
|
stdio: 'inherit'
|
|
475
529
|
};
|
|
476
530
|
}
|
|
531
|
+
function colorizeRunnerSteps(steps, env) {
|
|
532
|
+
return withPipedAppBootstrapImports(steps, env);
|
|
533
|
+
}
|
|
534
|
+
async function executeRunnerStepsWithReporter(options) {
|
|
535
|
+
if (options.command === 'dev' && (options.reporterMode === 'pretty' || options.verbose)) {
|
|
536
|
+
options.childEnv[SHOW_NODE_RESTART_NOTICE_ENV] = '1';
|
|
537
|
+
}
|
|
538
|
+
if (options.reporterMode === 'pretty') {
|
|
539
|
+
options.stdout.write(`[fluo] ${options.command} ${options.projectRuntime} lifecycle starting\n`);
|
|
540
|
+
options.stdout.write(`[fluo] ${options.runnerSteps.map(renderStep).join(' && ')}\n`);
|
|
541
|
+
}
|
|
542
|
+
const reporterStreams = createReporterStreams(options.reporterMode, options.verbose, options.stdout, options.stderr);
|
|
543
|
+
const exitCode = await runProjectRunnerSteps(options.runnerSteps, {
|
|
544
|
+
spawnCommand: options.runtime.spawnCommand ?? defaultSpawnCommand
|
|
545
|
+
}, {
|
|
546
|
+
cwd: options.projectDirectory,
|
|
547
|
+
env: options.childEnv,
|
|
548
|
+
...reporterStreams
|
|
549
|
+
});
|
|
550
|
+
if (options.reporterMode === 'pretty') {
|
|
551
|
+
reporterStreams.finalizeChildOutputBeforeStatus();
|
|
552
|
+
if (exitCode === 0) {
|
|
553
|
+
options.stdout.write(`[fluo] ${options.command} lifecycle completed\n`);
|
|
554
|
+
} else {
|
|
555
|
+
reporterStreams.flushBufferedStdoutOnFailure();
|
|
556
|
+
options.stderr.write(`[fluo] ${options.command} lifecycle failed with exit code ${exitCode}\n`);
|
|
557
|
+
}
|
|
558
|
+
} else if (options.reporterMode === 'silent' && exitCode !== 0) {
|
|
559
|
+
reporterStreams.flushBufferedStdoutOnFailure();
|
|
560
|
+
options.stderr.write(`[fluo] ${options.command} lifecycle failed with exit code ${exitCode}\n`);
|
|
561
|
+
}
|
|
562
|
+
return exitCode;
|
|
563
|
+
}
|
|
564
|
+
async function runScriptWithStudioSidecar(command, projectDirectory, projectRuntime, runnerSteps, childEnv, runtime, reporterMode, verbose, stdout, stderr, studioSidecar) {
|
|
565
|
+
try {
|
|
566
|
+
return await executeRunnerStepsWithReporter({
|
|
567
|
+
childEnv,
|
|
568
|
+
command,
|
|
569
|
+
projectDirectory,
|
|
570
|
+
projectRuntime,
|
|
571
|
+
reporterMode,
|
|
572
|
+
runnerSteps,
|
|
573
|
+
runtime,
|
|
574
|
+
stderr,
|
|
575
|
+
stdout,
|
|
576
|
+
verbose
|
|
577
|
+
});
|
|
578
|
+
} finally {
|
|
579
|
+
await studioSidecar.close();
|
|
580
|
+
}
|
|
581
|
+
}
|
|
477
582
|
|
|
478
583
|
/**
|
|
479
584
|
* Renders lifecycle command help text.
|
|
@@ -483,7 +588,7 @@ function createReporterStreams(mode, verbose, stdout, stderr) {
|
|
|
483
588
|
*/
|
|
484
589
|
export function scriptUsage(command) {
|
|
485
590
|
const nodeEnv = command === 'dev' ? 'development' : 'production';
|
|
486
|
-
return [`Usage: fluo ${command} [options] [-- <args>]`, '', `Run the generated fluo project ${command} lifecycle with NODE_ENV defaulting to ${nodeEnv} when unset.`, '', 'Default output forwards child stdout/stderr without fluo lifecycle UI.', 'Use --reporter pretty for fluo lifecycle status + app │ prefixes.', 'Use --verbose (or FLUO_VERBOSE=1) to expose raw runtime/tooling output.', '', 'Options', ' --dry-run Print the command without running it.', command === 'dev' ? ' --raw-watch Use the runtime-native Node watcher instead of the fluo restart runner.' : undefined, command === 'dev' ? ' --runner <fluo|native> Select fluo restart supervision or runtime-native watch (default: fluo for Node, native for non-Node runtimes).' : undefined, ' --reporter <auto|pretty|stream|silent> Choose lifecycle reporter output mode (default: auto).', ' --verbose Expose raw child process output; also honored by FLUO_VERBOSE=1.', ` --help Show help for the ${command} command.`].filter(line => typeof line === 'string').join('\n');
|
|
591
|
+
return [`Usage: fluo ${command} [options] [-- <args>]`, '', `Run the generated fluo project ${command} lifecycle with NODE_ENV defaulting to ${nodeEnv} when unset.`, '', 'Default output forwards child stdout/stderr without fluo lifecycle UI.', 'Use --reporter pretty for fluo lifecycle status + app │ prefixes.', 'Use --verbose (or FLUO_VERBOSE=1) to expose raw runtime/tooling output.', '', 'Options', ' --dry-run Print the command without running it.', command === 'dev' ? ' --raw-watch Use the runtime-native Node watcher instead of the fluo restart runner.' : undefined, command === 'dev' ? ' --runner <fluo|native> Select fluo restart supervision or runtime-native watch (default: fluo for Node, native for non-Node runtimes).' : undefined, command === 'dev' ? ' --studio Start the local Fluo Studio sidecar and inject runtime devtool env.' : undefined, command === 'dev' ? ' --studio-port <port> Bind the Studio sidecar to a specific local port (default: 0).' : undefined, ' --reporter <auto|pretty|stream|silent> Choose lifecycle reporter output mode (default: auto).', ' --verbose Expose raw child process output; also honored by FLUO_VERBOSE=1.', ` --help Show help for the ${command} command.`].filter(line => typeof line === 'string').join('\n');
|
|
487
592
|
}
|
|
488
593
|
|
|
489
594
|
/**
|
|
@@ -514,6 +619,7 @@ export async function runScriptCommand(command, argv, runtime = {}) {
|
|
|
514
619
|
throw new Error('--runner is only supported for fluo dev. Use -- to forward --runner to the child command.');
|
|
515
620
|
}
|
|
516
621
|
const devRunner = command === 'dev' ? resolveDevRunnerPreference(parsed, env, projectRuntime) : 'fluo';
|
|
622
|
+
assertStudioSupport(command, parsed.studio, projectRuntime, devRunner);
|
|
517
623
|
const runnerSteps = buildProjectRunner(command, projectRuntime, parsed.passThrough, {
|
|
518
624
|
devRunner,
|
|
519
625
|
rawWatch
|
|
@@ -524,8 +630,26 @@ export async function runScriptCommand(command, argv, runtime = {}) {
|
|
|
524
630
|
stdout
|
|
525
631
|
});
|
|
526
632
|
const verbose = parsed.verbose || isEnabledEnvironmentFlag(env.FLUO_VERBOSE);
|
|
527
|
-
|
|
528
|
-
|
|
633
|
+
let childEnv = withPipedReporterColorEnv(withProjectLocalBin(withDefaultNodeEnv(env, defaultNodeEnv), project.directory), reporterMode, stdout, stderr);
|
|
634
|
+
if (parsed.studio && parsed.dryRun) {
|
|
635
|
+
childEnv = withStudioDryRunEnv(childEnv, project, projectRuntime);
|
|
636
|
+
}
|
|
637
|
+
if (command === 'dev' && parsed.studio && !parsed.dryRun) {
|
|
638
|
+
const studioSidecarFactory = runtime.startStudioSidecar ?? startStudioSidecar;
|
|
639
|
+
const studioSidecar = await studioSidecarFactory({
|
|
640
|
+
appId: projectDisplayName(project),
|
|
641
|
+
port: parsed.studioPort,
|
|
642
|
+
runtime: projectRuntimeToStudioRuntime(projectRuntime)
|
|
643
|
+
});
|
|
644
|
+
childEnv = {
|
|
645
|
+
...childEnv,
|
|
646
|
+
...studioSidecar.env
|
|
647
|
+
};
|
|
648
|
+
const studioUrl = `${studioSidecar.url}/?token=${encodeURIComponent(studioSidecar.token)}`;
|
|
649
|
+
stdout.write(`[fluo] Studio listening at ${studioUrl}\n`);
|
|
650
|
+
return await runScriptWithStudioSidecar(command, project.directory, projectRuntime, colorizeRunnerSteps(runnerSteps, childEnv), childEnv, runtime, reporterMode, verbose, stdout, stderr, studioSidecar);
|
|
651
|
+
}
|
|
652
|
+
const colorAwareRunnerSteps = colorizeRunnerSteps(runnerSteps, childEnv);
|
|
529
653
|
if (command === 'dev' && (reporterMode === 'pretty' || verbose)) {
|
|
530
654
|
childEnv[SHOW_NODE_RESTART_NOTICE_ENV] = '1';
|
|
531
655
|
}
|
|
@@ -539,32 +663,24 @@ export async function runScriptCommand(command, argv, runtime = {}) {
|
|
|
539
663
|
stdout.write(`Reporter: ${reporterMode}\n`);
|
|
540
664
|
if (command === 'dev') {
|
|
541
665
|
stdout.write(`Watch mode: ${colorAwareRunnerSteps.map(step => step.mode ?? 'single-run').join(', ')}\n`);
|
|
666
|
+
if (parsed.studio) {
|
|
667
|
+
stdout.write('Studio: enabled (sidecar binds 127.0.0.1 at runtime)\n');
|
|
668
|
+
stdout.write(`FLUO_STUDIO: ${childEnv.FLUO_STUDIO ?? ''}\n`);
|
|
669
|
+
stdout.write(`FLUO_STUDIO_URL: ${childEnv.FLUO_STUDIO_URL ?? ''}\n`);
|
|
670
|
+
}
|
|
542
671
|
}
|
|
543
672
|
return 0;
|
|
544
673
|
}
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
674
|
+
return await executeRunnerStepsWithReporter({
|
|
675
|
+
childEnv,
|
|
676
|
+
command,
|
|
677
|
+
projectDirectory: project.directory,
|
|
678
|
+
projectRuntime,
|
|
679
|
+
reporterMode,
|
|
680
|
+
runnerSteps: colorAwareRunnerSteps,
|
|
681
|
+
runtime,
|
|
682
|
+
stderr,
|
|
683
|
+
stdout,
|
|
684
|
+
verbose
|
|
556
685
|
});
|
|
557
|
-
if (reporterMode === 'pretty') {
|
|
558
|
-
reporterStreams.finalizeChildOutputBeforeStatus();
|
|
559
|
-
if (exitCode === 0) {
|
|
560
|
-
stdout.write(`[fluo] ${command} lifecycle completed\n`);
|
|
561
|
-
} else {
|
|
562
|
-
reporterStreams.flushBufferedStdoutOnFailure();
|
|
563
|
-
stderr.write(`[fluo] ${command} lifecycle failed with exit code ${exitCode}\n`);
|
|
564
|
-
}
|
|
565
|
-
} else if (reporterMode === 'silent' && exitCode !== 0) {
|
|
566
|
-
reporterStreams.flushBufferedStdoutOnFailure();
|
|
567
|
-
stderr.write(`[fluo] ${command} lifecycle failed with exit code ${exitCode}\n`);
|
|
568
|
-
}
|
|
569
|
-
return exitCode;
|
|
570
686
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node-restart-runner.d.ts","sourceRoot":"","sources":["../../src/dev-runner/node-restart-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"node-restart-runner.d.ts","sourceRoot":"","sources":["../../src/dev-runner/node-restart-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,OAAO,EAA0D,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAKjG,KAAK,mBAAmB,GAAG;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACjC,CAAC;AAEF,KAAK,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAAC,KAAK,EAAE,SAAS,CAAA;CAAE,KAAK,YAAY,CAAC;AACjJ,2EAA2E;AAC3E,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,oBAAoB,GAAG,MAAM,GAAG,MAAM,CAAC;AAE9E,KAAK,aAAa,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE1C,KAAK,mBAAmB,GAAG;IACzB,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;IAC1D,IAAI,CAAC,MAAM,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC;CAC5D,CAAC;AAEF,KAAK,qBAAqB,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,iBAAiB,EAAE;IAAE,SAAS,EAAE,OAAO,CAAA;CAAE,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,KAAK,IAAI,KAAK,SAAS,CAAC;AAE1O,KAAK,iBAAiB,GAAG;IACvB,cAAc,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC;IAC9C,mBAAmB,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;CACvD,CAAC;AAEF,uFAAuF;AACvF,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,EAAE,gBAAgB,CAAC;IAC3B,YAAY,CAAC,EAAE,mBAAmB,CAAC;IACnC,UAAU,CAAC,EAAE,mBAAmB,CAAC;IACjC,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,MAAM,CAAC,EAAE,mBAAmB,CAAC;IAC7B,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC,CAAC;AA2LF;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,gBAAgB,EAAE,MAAM,EAAE,cAAc,GAAE,MAAM,EAAoB,GAAG,iBAAiB,CAuB/H;AAkFD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,CAiP7F"}
|
|
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, readdirSync, readFileSync, statSync, watch } from 'node:fs';
|
|
4
4
|
import { basename, dirname, join, relative, sep } from 'node:path';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { createStudioDevtoolsNodeImport } from '../studio/runtime-config.js';
|
|
6
7
|
|
|
7
8
|
/** Runtime target handled by the fluo-owned development restart runner. */
|
|
8
9
|
|
|
@@ -15,6 +16,53 @@ const DEFAULT_IGNORES = ['.cache', '.fluo', '.git', '.turbo', 'coverage', 'dist'
|
|
|
15
16
|
const WATCH_FILES = ['.env', 'package.json', 'tsconfig.json', 'tsconfig.build.json'];
|
|
16
17
|
const SHOW_NODE_RESTART_NOTICE_ENV = 'FLUO_DEV_SHOW_RESTART_NOTICE';
|
|
17
18
|
const CLEAR_SCREEN = '\u001B[2J\u001B[3J\u001B[H';
|
|
19
|
+
function isEnabledEnvironmentFlag(value) {
|
|
20
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
21
|
+
}
|
|
22
|
+
function studioRuntimeName(runtime) {
|
|
23
|
+
if (runtime === 'cloudflare-workers') {
|
|
24
|
+
return 'worker';
|
|
25
|
+
}
|
|
26
|
+
return runtime;
|
|
27
|
+
}
|
|
28
|
+
function resolveStudioIngestEndpoint(env) {
|
|
29
|
+
if (!isEnabledEnvironmentFlag(env.FLUO_STUDIO) || !env.FLUO_STUDIO_TOKEN) {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
if (env.FLUO_STUDIO_ENDPOINT) {
|
|
33
|
+
return env.FLUO_STUDIO_ENDPOINT;
|
|
34
|
+
}
|
|
35
|
+
if (!env.FLUO_STUDIO_URL) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
return new URL('/api/runtime/events', env.FLUO_STUDIO_URL).toString();
|
|
40
|
+
} catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function publishStudioLifecycleEvent(env, runtime, type, payload) {
|
|
45
|
+
const endpoint = resolveStudioIngestEndpoint(env);
|
|
46
|
+
if (!endpoint || typeof globalThis.fetch !== 'function') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
void globalThis.fetch(endpoint, {
|
|
50
|
+
body: JSON.stringify({
|
|
51
|
+
payload,
|
|
52
|
+
source: {
|
|
53
|
+
appId: env.FLUO_STUDIO_APP_ID ?? basename(process.cwd()),
|
|
54
|
+
runtime: studioRuntimeName(runtime)
|
|
55
|
+
},
|
|
56
|
+
type,
|
|
57
|
+
version: 1
|
|
58
|
+
}),
|
|
59
|
+
headers: {
|
|
60
|
+
authorization: `Bearer ${env.FLUO_STUDIO_TOKEN}`,
|
|
61
|
+
'content-type': 'application/json'
|
|
62
|
+
},
|
|
63
|
+
method: 'POST'
|
|
64
|
+
}).catch(() => undefined);
|
|
65
|
+
}
|
|
18
66
|
function normalizeIgnorePatterns(patterns) {
|
|
19
67
|
return patterns.map(pattern => pattern.trim()).filter(pattern => pattern.length > 0);
|
|
20
68
|
}
|
|
@@ -151,7 +199,8 @@ function getPreserveColorTtyImport() {
|
|
|
151
199
|
}
|
|
152
200
|
function buildNodeAppArgs(env, appArgs) {
|
|
153
201
|
const colorTtyImport = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--import', getPreserveColorTtyImport()] : [];
|
|
154
|
-
|
|
202
|
+
const studioDevtoolsImport = createStudioDevtoolsNodeImport(env);
|
|
203
|
+
return ['--env-file=.env', ...colorTtyImport, ...studioDevtoolsImport, '--import', 'tsx', 'src/main.ts', ...appArgs];
|
|
155
204
|
}
|
|
156
205
|
function buildBunAppArgs(env, appArgs) {
|
|
157
206
|
const colorTtyPreload = env[PRETTY_TTY_COLOR_ENV] === '1' ? ['--preload', getPreserveColorTtyImport()] : [];
|
|
@@ -230,11 +279,19 @@ export async function runNodeRestartRunner(options) {
|
|
|
230
279
|
let stopping = false;
|
|
231
280
|
const startChild = (resolveExitCode, cleanup) => {
|
|
232
281
|
const appCommand = buildAppCommand(runnerRuntime, env, appArgs);
|
|
282
|
+
publishStudioLifecycleEvent(env, runnerRuntime, 'restart', {
|
|
283
|
+
phase: 'starting',
|
|
284
|
+
reason: 'fluo dev runner starting app child'
|
|
285
|
+
});
|
|
233
286
|
child = spawnChild(appCommand.command, appCommand.args, {
|
|
234
287
|
cwd: projectDirectory,
|
|
235
288
|
env,
|
|
236
289
|
stdio: 'inherit'
|
|
237
290
|
});
|
|
291
|
+
publishStudioLifecycleEvent(env, runnerRuntime, 'restart', {
|
|
292
|
+
phase: 'started',
|
|
293
|
+
reason: 'fluo dev runner spawned app child'
|
|
294
|
+
});
|
|
238
295
|
let childSettled = false;
|
|
239
296
|
child.once('error', error => {
|
|
240
297
|
if (childSettled) {
|
|
@@ -255,10 +312,16 @@ export async function runNodeRestartRunner(options) {
|
|
|
255
312
|
return;
|
|
256
313
|
}
|
|
257
314
|
if (stopping) {
|
|
315
|
+
publishStudioLifecycleEvent(env, runnerRuntime, 'disconnect', {
|
|
316
|
+
reason: 'fluo dev runner stopped'
|
|
317
|
+
});
|
|
258
318
|
cleanup();
|
|
259
319
|
resolveExitCode(code ?? 0);
|
|
260
320
|
return;
|
|
261
321
|
}
|
|
322
|
+
publishStudioLifecycleEvent(env, runnerRuntime, 'disconnect', {
|
|
323
|
+
reason: `app child exited with code ${String(code ?? 1)}`
|
|
324
|
+
});
|
|
262
325
|
cleanup();
|
|
263
326
|
resolveExitCode(code ?? 1);
|
|
264
327
|
});
|
|
@@ -278,6 +341,10 @@ export async function runNodeRestartRunner(options) {
|
|
|
278
341
|
if (env[SHOW_NODE_RESTART_NOTICE_ENV] === '1') {
|
|
279
342
|
stdout.write(`[fluo] restarting after content change: ${relative(projectDirectory, restartPaths[restartPaths.length - 1] ?? projectDirectory)}\n`);
|
|
280
343
|
}
|
|
344
|
+
publishStudioLifecycleEvent(env, runnerRuntime, 'restart', {
|
|
345
|
+
phase: 'scheduled',
|
|
346
|
+
reason: `content changed: ${relative(projectDirectory, restartPaths[restartPaths.length - 1] ?? projectDirectory)}`
|
|
347
|
+
});
|
|
281
348
|
const previousChild = child;
|
|
282
349
|
const startReplacementChild = () => {
|
|
283
350
|
redrawDevScriptHeader(stdout, projectDirectory, env);
|
|
@@ -292,6 +359,10 @@ export async function runNodeRestartRunner(options) {
|
|
|
292
359
|
return;
|
|
293
360
|
}
|
|
294
361
|
restarting = true;
|
|
362
|
+
publishStudioLifecycleEvent(env, runnerRuntime, 'restart', {
|
|
363
|
+
phase: 'stopping',
|
|
364
|
+
reason: 'stopping previous app child before restart'
|
|
365
|
+
});
|
|
295
366
|
previousChild.once('close', () => {
|
|
296
367
|
const committedRestartPaths = [...restartAfterClosePaths];
|
|
297
368
|
restartAfterClosePaths.clear();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export declare const STUDIO_DEVTOOLS_GLOBAL_CONFIG_KEY = "__FLUO_STUDIO_DEVTOOLS_CONFIG__";
|
|
2
|
+
export interface StudioDevtoolsInjectedConfig {
|
|
3
|
+
FLUO_STUDIO: '1';
|
|
4
|
+
FLUO_STUDIO_APP_ID?: string;
|
|
5
|
+
FLUO_STUDIO_ENDPOINT?: string;
|
|
6
|
+
FLUO_STUDIO_EPOCH?: string;
|
|
7
|
+
FLUO_STUDIO_RUNTIME?: string;
|
|
8
|
+
FLUO_STUDIO_TOKEN: string;
|
|
9
|
+
FLUO_STUDIO_URL?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Builds the explicit Studio config that CLI-owned dev boundaries inject into app children.
|
|
13
|
+
*
|
|
14
|
+
* Package runtime code must not read ambient environment state directly. The CLI sidecar still stores the
|
|
15
|
+
* generated app id/token/URL in child environment for process supervision, then this helper converts
|
|
16
|
+
* those values into a typed process-local config object before the app imports `@fluojs/runtime`.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveStudioDevtoolsInjectedConfig(env: NodeJS.ProcessEnv): StudioDevtoolsInjectedConfig | undefined;
|
|
19
|
+
export declare function createStudioDevtoolsNodeImport(env: NodeJS.ProcessEnv): string[];
|
|
20
|
+
//# sourceMappingURL=runtime-config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime-config.d.ts","sourceRoot":"","sources":["../../src/studio/runtime-config.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iCAAiC,oCAAoC,CAAC;AAEnF,MAAM,WAAW,4BAA4B;IAC3C,WAAW,EAAE,GAAG,CAAC;IACjB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAsBD;;;;;;GAMG;AACH,wBAAgB,mCAAmC,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,4BAA4B,GAAG,SAAS,CAmBpH;AAED,wBAAgB,8BAA8B,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,GAAG,MAAM,EAAE,CAS/E"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export const STUDIO_DEVTOOLS_GLOBAL_CONFIG_KEY = '__FLUO_STUDIO_DEVTOOLS_CONFIG__';
|
|
2
|
+
function isEnabledEnvironmentFlag(value) {
|
|
3
|
+
return value === '1' || value === 'true' || value === 'yes' || value === 'on';
|
|
4
|
+
}
|
|
5
|
+
function resolveStudioIngestEndpoint(env) {
|
|
6
|
+
if (env.FLUO_STUDIO_ENDPOINT) {
|
|
7
|
+
return env.FLUO_STUDIO_ENDPOINT;
|
|
8
|
+
}
|
|
9
|
+
if (!env.FLUO_STUDIO_URL) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
return new URL('/api/runtime/events', env.FLUO_STUDIO_URL).toString();
|
|
14
|
+
} catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Builds the explicit Studio config that CLI-owned dev boundaries inject into app children.
|
|
21
|
+
*
|
|
22
|
+
* Package runtime code must not read ambient environment state directly. The CLI sidecar still stores the
|
|
23
|
+
* generated app id/token/URL in child environment for process supervision, then this helper converts
|
|
24
|
+
* those values into a typed process-local config object before the app imports `@fluojs/runtime`.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveStudioDevtoolsInjectedConfig(env) {
|
|
27
|
+
if (!isEnabledEnvironmentFlag(env.FLUO_STUDIO) || !env.FLUO_STUDIO_TOKEN) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
const endpoint = resolveStudioIngestEndpoint(env);
|
|
31
|
+
if (!endpoint) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
FLUO_STUDIO: '1',
|
|
36
|
+
FLUO_STUDIO_APP_ID: env.FLUO_STUDIO_APP_ID,
|
|
37
|
+
FLUO_STUDIO_ENDPOINT: endpoint,
|
|
38
|
+
FLUO_STUDIO_EPOCH: env.FLUO_STUDIO_EPOCH,
|
|
39
|
+
FLUO_STUDIO_RUNTIME: env.FLUO_STUDIO_RUNTIME,
|
|
40
|
+
FLUO_STUDIO_TOKEN: env.FLUO_STUDIO_TOKEN,
|
|
41
|
+
FLUO_STUDIO_URL: env.FLUO_STUDIO_URL
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function createStudioDevtoolsNodeImport(env) {
|
|
45
|
+
const config = resolveStudioDevtoolsInjectedConfig(env);
|
|
46
|
+
if (!config) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const source = `Object.defineProperty(globalThis, ${JSON.stringify(STUDIO_DEVTOOLS_GLOBAL_CONFIG_KEY)}, { configurable: true, enumerable: false, writable: true, value: ${JSON.stringify(config)} });`;
|
|
50
|
+
return ['--import', `data:text/javascript,${encodeURIComponent(source)}`];
|
|
51
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defines Studio Sidecar Runtime values used by the Studio devtool.
|
|
3
|
+
*/
|
|
4
|
+
export type StudioSidecarRuntime = 'bun' | 'deno' | 'node' | 'unknown';
|
|
5
|
+
/**
|
|
6
|
+
* Describes Studio Sidecar Options data used by the Studio devtool.
|
|
7
|
+
*/
|
|
8
|
+
export interface StudioSidecarOptions {
|
|
9
|
+
appId?: string;
|
|
10
|
+
heartbeatMs?: number;
|
|
11
|
+
host?: string;
|
|
12
|
+
port?: number;
|
|
13
|
+
runtime?: StudioSidecarRuntime;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Describes Studio Sidecar data used by the Studio devtool.
|
|
17
|
+
*/
|
|
18
|
+
export interface StudioSidecar {
|
|
19
|
+
readonly appId: string;
|
|
20
|
+
readonly epoch: string;
|
|
21
|
+
readonly env: NodeJS.ProcessEnv;
|
|
22
|
+
readonly host: string;
|
|
23
|
+
readonly port: number;
|
|
24
|
+
readonly token: string;
|
|
25
|
+
readonly url: string;
|
|
26
|
+
close(): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Provides start Studio Sidecar behavior for the Studio devtool.
|
|
30
|
+
*
|
|
31
|
+
* @param options options value used by start Studio Sidecar.
|
|
32
|
+
* @returns The start Studio Sidecar result.
|
|
33
|
+
*/
|
|
34
|
+
export declare function startStudioSidecar(options?: StudioSidecarOptions): Promise<StudioSidecar>;
|
|
35
|
+
//# sourceMappingURL=sidecar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../../src/studio/sidecar.ts"],"names":[],"mappings":"AAOA;;GAEG;AACH,MAAM,MAAM,oBAAoB,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEvE;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,oBAAoB,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAkPD;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,OAAO,GAAE,oBAAyB,GAAG,OAAO,CAAC,aAAa,CAAC,CAiLnG"}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createReadStream, existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
|
+
import { createServer } from 'node:http';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, extname, join, normalize, relative, sep } from 'node:path';
|
|
6
|
+
import { URL } from 'node:url';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Defines Studio Sidecar Runtime values used by the Studio devtool.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Describes Studio Sidecar Options data used by the Studio devtool.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Describes Studio Sidecar data used by the Studio devtool.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
21
|
+
const DEFAULT_HEARTBEAT_MS = 15_000;
|
|
22
|
+
const MAX_EVENT_REPLAY = 1_000;
|
|
23
|
+
const MAX_REQUEST_BYTES = 1_048_576;
|
|
24
|
+
const require = createRequire(import.meta.url);
|
|
25
|
+
function isRecord(value) {
|
|
26
|
+
return typeof value === 'object' && value !== null;
|
|
27
|
+
}
|
|
28
|
+
function isRestartEpochBoundary(incoming) {
|
|
29
|
+
if (incoming.type !== 'restart' || !isRecord(incoming.payload)) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
return incoming.payload.phase === 'scheduled' || incoming.payload.phase === 'starting';
|
|
33
|
+
}
|
|
34
|
+
function createToken() {
|
|
35
|
+
return randomBytes(24).toString('base64url');
|
|
36
|
+
}
|
|
37
|
+
function createEpoch() {
|
|
38
|
+
return randomUUID();
|
|
39
|
+
}
|
|
40
|
+
function createDefaultAppId() {
|
|
41
|
+
return `fluo-app-${process.pid}`;
|
|
42
|
+
}
|
|
43
|
+
function readBody(request) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
let body = '';
|
|
46
|
+
request.setEncoding('utf8');
|
|
47
|
+
request.on('data', chunk => {
|
|
48
|
+
body += chunk;
|
|
49
|
+
if (body.length > MAX_REQUEST_BYTES) {
|
|
50
|
+
reject(new Error('Studio event payload is too large.'));
|
|
51
|
+
request.destroy();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
request.on('end', () => resolve(body));
|
|
55
|
+
request.on('error', reject);
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function writeJson(response, statusCode, payload) {
|
|
59
|
+
response.writeHead(statusCode, {
|
|
60
|
+
'cache-control': 'no-store',
|
|
61
|
+
'content-type': 'application/json; charset=utf-8'
|
|
62
|
+
});
|
|
63
|
+
response.end(JSON.stringify(payload));
|
|
64
|
+
}
|
|
65
|
+
function writeText(response, statusCode, body, contentType = 'text/plain; charset=utf-8') {
|
|
66
|
+
response.writeHead(statusCode, {
|
|
67
|
+
'cache-control': 'no-store',
|
|
68
|
+
'content-type': contentType
|
|
69
|
+
});
|
|
70
|
+
response.end(body);
|
|
71
|
+
}
|
|
72
|
+
function contentTypeForPath(pathname) {
|
|
73
|
+
switch (extname(pathname)) {
|
|
74
|
+
case '.css':
|
|
75
|
+
return 'text/css; charset=utf-8';
|
|
76
|
+
case '.html':
|
|
77
|
+
return 'text/html; charset=utf-8';
|
|
78
|
+
case '.js':
|
|
79
|
+
return 'text/javascript; charset=utf-8';
|
|
80
|
+
case '.json':
|
|
81
|
+
return 'application/json; charset=utf-8';
|
|
82
|
+
case '.map':
|
|
83
|
+
return 'application/json; charset=utf-8';
|
|
84
|
+
case '.svg':
|
|
85
|
+
return 'image/svg+xml';
|
|
86
|
+
case '.woff2':
|
|
87
|
+
return 'font/woff2';
|
|
88
|
+
default:
|
|
89
|
+
return 'application/octet-stream';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function resolveStudioViewerPath() {
|
|
93
|
+
try {
|
|
94
|
+
const viewerPath = require.resolve('@fluojs/studio/viewer');
|
|
95
|
+
return existsSync(viewerPath) ? viewerPath : undefined;
|
|
96
|
+
} catch {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function injectStudioConfig(html, options) {
|
|
101
|
+
const configScript = `<script>window.__FLUO_STUDIO__ = ${JSON.stringify(options).replaceAll('<', '\\u003c')};</script>`;
|
|
102
|
+
if (html.includes('</head>')) {
|
|
103
|
+
return html.replace('</head>', ` ${configScript}\n</head>`);
|
|
104
|
+
}
|
|
105
|
+
return `${configScript}\n${html}`;
|
|
106
|
+
}
|
|
107
|
+
function safeAssetPath(rootDirectory, pathname) {
|
|
108
|
+
let decodedPath;
|
|
109
|
+
try {
|
|
110
|
+
decodedPath = decodeURIComponent(pathname);
|
|
111
|
+
} catch {
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const normalized = normalize(decodedPath).replace(/^[/\\]+/, '');
|
|
115
|
+
const candidate = join(rootDirectory, normalized);
|
|
116
|
+
const relativePath = relative(rootDirectory, candidate);
|
|
117
|
+
if (relativePath.startsWith('..') || relativePath.split(sep).includes('..')) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
return candidate;
|
|
121
|
+
}
|
|
122
|
+
function serveStudioAsset(response, rootDirectory, pathname) {
|
|
123
|
+
const assetPath = safeAssetPath(rootDirectory, pathname);
|
|
124
|
+
if (!assetPath || !existsSync(assetPath)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const stats = statSync(assetPath);
|
|
128
|
+
if (!stats.isFile()) {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
response.writeHead(200, {
|
|
132
|
+
'cache-control': 'no-store',
|
|
133
|
+
'content-length': String(stats.size),
|
|
134
|
+
'content-type': contentTypeForPath(assetPath)
|
|
135
|
+
});
|
|
136
|
+
createReadStream(assetPath).pipe(response);
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
function extractBearerToken(request) {
|
|
140
|
+
const authorization = request.headers.authorization;
|
|
141
|
+
if (typeof authorization !== 'string') {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
const [scheme, token] = authorization.split(' ');
|
|
145
|
+
return scheme.toLowerCase() === 'bearer' && token ? token : undefined;
|
|
146
|
+
}
|
|
147
|
+
function requestToken(request, url) {
|
|
148
|
+
return extractBearerToken(request) ?? url.searchParams.get('token') ?? undefined;
|
|
149
|
+
}
|
|
150
|
+
function isAuthorized(request, url, token) {
|
|
151
|
+
return requestToken(request, url) === token;
|
|
152
|
+
}
|
|
153
|
+
function parseAfterSequence(url, request, epoch) {
|
|
154
|
+
const after = url.searchParams.get('after') ?? request.headers['last-event-id'];
|
|
155
|
+
const value = Array.isArray(after) ? after[0] : after;
|
|
156
|
+
if (!value) {
|
|
157
|
+
return 0;
|
|
158
|
+
}
|
|
159
|
+
if (/^\d+$/.test(value)) {
|
|
160
|
+
return Number(value);
|
|
161
|
+
}
|
|
162
|
+
const [eventEpoch, sequence] = value.split(':');
|
|
163
|
+
if (eventEpoch === epoch && /^\d+$/.test(sequence)) {
|
|
164
|
+
return Number(sequence);
|
|
165
|
+
}
|
|
166
|
+
return 0;
|
|
167
|
+
}
|
|
168
|
+
function writeSseEvent(response, event) {
|
|
169
|
+
response.write(`id: ${event.eventId}\n`);
|
|
170
|
+
response.write(`event: ${event.type}\n`);
|
|
171
|
+
response.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
172
|
+
}
|
|
173
|
+
function renderStudioShell(options) {
|
|
174
|
+
const viewerPath = resolveStudioViewerPath();
|
|
175
|
+
if (viewerPath) {
|
|
176
|
+
return injectStudioConfig(readFileSync(viewerPath, 'utf8'), options);
|
|
177
|
+
}
|
|
178
|
+
const config = JSON.stringify(options).replaceAll('<', '\\u003c');
|
|
179
|
+
return `<!doctype html>
|
|
180
|
+
<html lang="en">
|
|
181
|
+
<head>
|
|
182
|
+
<meta charset="utf-8" />
|
|
183
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
184
|
+
<title>Fluo Studio</title>
|
|
185
|
+
<style>
|
|
186
|
+
:root { color-scheme: dark; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
187
|
+
body { margin: 0; background: #0a0f1d; color: #dbeafe; }
|
|
188
|
+
main { min-height: 100vh; display: grid; place-items: center; padding: 32px; }
|
|
189
|
+
section { max-width: 760px; border: 1px solid rgba(148, 163, 184, 0.22); border-radius: 24px; padding: 32px; background: linear-gradient(135deg, rgba(15, 23, 42, 0.96), rgba(30, 41, 59, 0.82)); box-shadow: 0 24px 80px rgba(0, 0, 0, 0.28); }
|
|
190
|
+
h1 { margin: 0 0 12px; font-size: 34px; letter-spacing: -0.04em; }
|
|
191
|
+
p { color: #94a3b8; line-height: 1.7; }
|
|
192
|
+
code { color: #93c5fd; }
|
|
193
|
+
</style>
|
|
194
|
+
</head>
|
|
195
|
+
<body>
|
|
196
|
+
<main>
|
|
197
|
+
<section>
|
|
198
|
+
<h1>Fluo Studio sidecar is live</h1>
|
|
199
|
+
<p>This token-protected local sidecar is receiving runtime events. The React Studio UI will attach to <code>/api/events</code> in the next package layer.</p>
|
|
200
|
+
<p>Config: <code id="config"></code></p>
|
|
201
|
+
</section>
|
|
202
|
+
</main>
|
|
203
|
+
<script>window.__FLUO_STUDIO__ = ${config}; document.getElementById('config').textContent = JSON.stringify(window.__FLUO_STUDIO__);</script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Provides start Studio Sidecar behavior for the Studio devtool.
|
|
210
|
+
*
|
|
211
|
+
* @param options options value used by start Studio Sidecar.
|
|
212
|
+
* @returns The start Studio Sidecar result.
|
|
213
|
+
*/
|
|
214
|
+
export async function startStudioSidecar(options = {}) {
|
|
215
|
+
const host = options.host ?? DEFAULT_HOST;
|
|
216
|
+
const appId = options.appId ?? createDefaultAppId();
|
|
217
|
+
const runtime = options.runtime ?? 'node';
|
|
218
|
+
let epoch = createEpoch();
|
|
219
|
+
const token = createToken();
|
|
220
|
+
const events = [];
|
|
221
|
+
const clients = new Set();
|
|
222
|
+
let sequence = 0;
|
|
223
|
+
const startedAt = performance.now();
|
|
224
|
+
const publish = incoming => {
|
|
225
|
+
if (isRestartEpochBoundary(incoming)) {
|
|
226
|
+
epoch = createEpoch();
|
|
227
|
+
}
|
|
228
|
+
sequence += 1;
|
|
229
|
+
const source = isRecord(incoming.source) ? incoming.source : undefined;
|
|
230
|
+
const sourceAppId = typeof source?.appId === 'string' && source.appId.length > 0 ? source.appId : appId;
|
|
231
|
+
const sourceRuntime = source?.runtime === 'bun' || source?.runtime === 'deno' || source?.runtime === 'node' ? source.runtime : runtime;
|
|
232
|
+
const event = {
|
|
233
|
+
emittedAt: new Date().toISOString(),
|
|
234
|
+
epoch,
|
|
235
|
+
eventId: `${epoch}:${String(sequence)}`,
|
|
236
|
+
payload: incoming.payload ?? {},
|
|
237
|
+
sequence,
|
|
238
|
+
source: {
|
|
239
|
+
appId: sourceAppId,
|
|
240
|
+
runtime: sourceRuntime
|
|
241
|
+
},
|
|
242
|
+
type: typeof incoming.type === 'string' && incoming.type.length > 0 ? incoming.type : 'diagnostic',
|
|
243
|
+
version: 1
|
|
244
|
+
};
|
|
245
|
+
events.push(event);
|
|
246
|
+
if (events.length > MAX_EVENT_REPLAY) {
|
|
247
|
+
events.splice(0, events.length - MAX_EVENT_REPLAY);
|
|
248
|
+
}
|
|
249
|
+
for (const client of clients) {
|
|
250
|
+
writeSseEvent(client.response, event);
|
|
251
|
+
}
|
|
252
|
+
return event;
|
|
253
|
+
};
|
|
254
|
+
const server = createServer(async (request, response) => {
|
|
255
|
+
const requestUrl = new URL(request.url ?? '/', `http://${host}`);
|
|
256
|
+
const viewerPath = resolveStudioViewerPath();
|
|
257
|
+
if (request.method === 'GET' && requestUrl.pathname.startsWith('/assets/') && viewerPath) {
|
|
258
|
+
if (serveStudioAsset(response, dirname(viewerPath), requestUrl.pathname)) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!isAuthorized(request, requestUrl, token)) {
|
|
263
|
+
writeJson(response, 401, {
|
|
264
|
+
error: 'Unauthorized Studio sidecar request.'
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (request.method === 'GET' && requestUrl.pathname === '/') {
|
|
269
|
+
const tokenQuery = encodeURIComponent(token);
|
|
270
|
+
writeText(response, 200, renderStudioShell({
|
|
271
|
+
eventsUrl: `/api/events?token=${tokenQuery}`,
|
|
272
|
+
stateUrl: `/api/state?token=${tokenQuery}`
|
|
273
|
+
}), 'text/html; charset=utf-8');
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (request.method === 'GET' && requestUrl.pathname === '/api/state') {
|
|
277
|
+
writeJson(response, 200, {
|
|
278
|
+
appId,
|
|
279
|
+
clientCount: clients.size,
|
|
280
|
+
epoch,
|
|
281
|
+
events,
|
|
282
|
+
sequence
|
|
283
|
+
});
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (request.method === 'GET' && requestUrl.pathname === '/api/events') {
|
|
287
|
+
response.writeHead(200, {
|
|
288
|
+
'cache-control': 'no-cache, no-transform',
|
|
289
|
+
connection: 'keep-alive',
|
|
290
|
+
'content-type': 'text/event-stream; charset=utf-8',
|
|
291
|
+
'x-accel-buffering': 'no'
|
|
292
|
+
});
|
|
293
|
+
response.write(': fluo studio stream ready\n\n');
|
|
294
|
+
const afterSequence = requestUrl.searchParams.get('replay') === '0' ? sequence : parseAfterSequence(requestUrl, request, epoch);
|
|
295
|
+
for (const event of events) {
|
|
296
|
+
if (event.sequence > afterSequence) {
|
|
297
|
+
writeSseEvent(response, event);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
const client = {
|
|
301
|
+
response
|
|
302
|
+
};
|
|
303
|
+
clients.add(client);
|
|
304
|
+
request.on('close', () => {
|
|
305
|
+
clients.delete(client);
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (request.method === 'POST' && requestUrl.pathname === '/api/runtime/events') {
|
|
310
|
+
try {
|
|
311
|
+
const body = await readBody(request);
|
|
312
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
313
|
+
if (!isRecord(parsed)) {
|
|
314
|
+
writeJson(response, 400, {
|
|
315
|
+
error: 'Studio runtime event must be a JSON object.'
|
|
316
|
+
});
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const event = publish(parsed);
|
|
320
|
+
writeJson(response, 202, {
|
|
321
|
+
accepted: true,
|
|
322
|
+
epoch: event.epoch,
|
|
323
|
+
sequence: event.sequence
|
|
324
|
+
});
|
|
325
|
+
} catch (error) {
|
|
326
|
+
writeJson(response, 400, {
|
|
327
|
+
error: error instanceof Error ? error.message : String(error)
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
writeJson(response, 404, {
|
|
333
|
+
error: 'Unknown Studio sidecar route.'
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
const heartbeat = options.heartbeatMs === 0 ? undefined : setInterval(() => {
|
|
337
|
+
publish({
|
|
338
|
+
payload: {
|
|
339
|
+
uptimeMs: Number((performance.now() - startedAt).toFixed(3))
|
|
340
|
+
},
|
|
341
|
+
source: {
|
|
342
|
+
appId,
|
|
343
|
+
runtime
|
|
344
|
+
},
|
|
345
|
+
type: 'heartbeat'
|
|
346
|
+
});
|
|
347
|
+
}, options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS);
|
|
348
|
+
heartbeat?.unref();
|
|
349
|
+
await new Promise((resolve, reject) => {
|
|
350
|
+
server.once('error', reject);
|
|
351
|
+
server.listen(options.port ?? 0, host, () => {
|
|
352
|
+
server.off('error', reject);
|
|
353
|
+
resolve();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
const address = server.address();
|
|
357
|
+
if (!address || typeof address === 'string') {
|
|
358
|
+
await closeServer(server, clients, heartbeat);
|
|
359
|
+
throw new Error('Failed to resolve Studio sidecar address.');
|
|
360
|
+
}
|
|
361
|
+
const url = `http://${host}:${String(address.port)}`;
|
|
362
|
+
return {
|
|
363
|
+
appId,
|
|
364
|
+
get epoch() {
|
|
365
|
+
return epoch;
|
|
366
|
+
},
|
|
367
|
+
env: {
|
|
368
|
+
FLUO_STUDIO: '1',
|
|
369
|
+
FLUO_STUDIO_APP_ID: appId,
|
|
370
|
+
FLUO_STUDIO_EPOCH: epoch,
|
|
371
|
+
FLUO_STUDIO_RUNTIME: runtime,
|
|
372
|
+
FLUO_STUDIO_TOKEN: token,
|
|
373
|
+
FLUO_STUDIO_URL: url
|
|
374
|
+
},
|
|
375
|
+
host,
|
|
376
|
+
port: address.port,
|
|
377
|
+
token,
|
|
378
|
+
url,
|
|
379
|
+
async close() {
|
|
380
|
+
await closeServer(server, clients, heartbeat);
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
async function closeServer(server, clients, heartbeat) {
|
|
385
|
+
if (heartbeat) {
|
|
386
|
+
clearInterval(heartbeat);
|
|
387
|
+
}
|
|
388
|
+
for (const client of clients) {
|
|
389
|
+
client.response.end();
|
|
390
|
+
}
|
|
391
|
+
clients.clear();
|
|
392
|
+
await new Promise((resolve, reject) => {
|
|
393
|
+
server.close(error => {
|
|
394
|
+
if (error) {
|
|
395
|
+
reject(error);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
resolve();
|
|
399
|
+
});
|
|
400
|
+
});
|
|
401
|
+
}
|
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"migration",
|
|
10
10
|
"diagnostics"
|
|
11
11
|
],
|
|
12
|
-
"version": "1.0.
|
|
12
|
+
"version": "1.0.5",
|
|
13
13
|
"private": false,
|
|
14
14
|
"license": "MIT",
|
|
15
15
|
"repository": {
|
|
@@ -44,10 +44,10 @@
|
|
|
44
44
|
"ejs": "^3.1.10",
|
|
45
45
|
"tsx": "^4.20.4",
|
|
46
46
|
"typescript": "^6.0.2",
|
|
47
|
-
"@fluojs/runtime": "^1.1.
|
|
47
|
+
"@fluojs/runtime": "^1.1.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@fluojs/studio": "^1.0.
|
|
50
|
+
"@fluojs/studio": "^1.0.5"
|
|
51
51
|
},
|
|
52
52
|
"peerDependenciesMeta": {
|
|
53
53
|
"@fluojs/studio": {
|