Skip to content

feat(ts-builder): dev-server sidecar for block-ui serve#1657

Open
dbolotin wants to merge 4 commits into
mainfrom
feat/dev-server-sidecar
Open

feat(ts-builder): dev-server sidecar for block-ui serve#1657
dbolotin wants to merge 4 commits into
mainfrom
feat/dev-server-sidecar

Conversation

@dbolotin

@dbolotin dbolotin commented May 26, 2026

Copy link
Copy Markdown
Member

Summary

Adds dev-server sidecar mechanism: when block authors run ts-builder serve --target block-ui, ts-builder writes <ui>/dist/.dev-server (atomic JSON: schema, url, pid) signalling the URL where the live Vite dev server is reachable. The desktop app reads this file and routes the block view to Vite instead of the block-ui:// folder protocol, enabling HMR. File is removed on clean shutdown; the desktop app HEAD-probes the URL before using it, so a stale sidecar safely falls back to folder mode.

  • serve --target block-ui switches from spawning Vite to using its programmatic API so we can write the sidecar with the actually-bound URL (port may differ from 5173).
  • setupSidecarLifecycle() registers SIGINT/SIGTERM/SIGHUP/exit/uncaughtException cleanup hooks before createServer() to win the race against Vite's own signal handlers.
  • Dev-only Vite plugin (apply: "serve") injects 'wasm-unsafe-eval' into the index.html CSP meta tag so in-page WASM (used by SpecDriver) loads under dev. Production loads via block-ui:// with bypassCSP: true and is unchanged. A FOLLOW-UP block at the top of createViteDevConfig.ts flags the migration debt (drop bypassCSP everywhere + security review).
  • define: { "process.env": "({})" } so browser-side packages that runtime-reference process.env.* (e.g. pf-spec-driver/logging.ts) don't crash on load.

Consumer side (desktop app) lives in milaboratory/platforma-desktop-app# on branch feat/dev-server-sidecar.

Test plan

  • pnpm build clean
  • End-to-end smoke with enter-numbers-v3 block: serve → desktop loads via Vite, label edit hot-reloads
  • Sidecar removed on Ctrl-C / process exit
  • Stale sidecar (process SIGKILL'd) → desktop HEAD probe fails → folder fallback
  • Port collision (5173 taken by renderer) → Vite picks 5174 → sidecar carries 5174 → desktop loads correct URL

Greptile Summary

This PR introduces a dev-server sidecar mechanism for ts-builder serve --target block-ui: rather than spawning Vite as a child process, it uses Vite's programmatic API to discover the actual bound URL and atomically writes a .dev-server JSON file that the desktop app reads to enable HMR. It also adds a dev-only Vite plugin to relax the script-src CSP for WASM and a process.env shim to silence transitive Node references in the browser.

  • Sidecar lifecycle (dev-server-sidecar.ts): registers SIGINT/SIGTERM/SIGHUP/exit/uncaughtException hooks before vite.createServer() to win the race against Vite's own signal handlers; atomic rename prevents half-written reads; server.close() is monkey-patched to cover programmatic/HMR restart paths.
  • CSP plugin (createViteDevConfig.ts): apply: \"serve\" scoped plugin rewrites the index.html CSP meta tag to add 'wasm-unsafe-eval'; accompanied by a detailed follow-up block listing migration debt.
  • process.env define: not guarded by isServe, which may affect production vite build runs; the CSP regex also requires a specific attribute order that could silently miss non-canonical <meta> tags.

Confidence Score: 3/5

Mostly safe to merge for dev-only use, but the unconditional process.env define warrants a check before landing.

The core sidecar mechanism is well-designed and the signal-ordering rationale is sound. The main concern is the process.env define being applied without an isServe guard — if createViteDevConfig is used for production builds, process.env.NODE_ENV will be undefined in bundles, potentially disabling dead-code elimination and leaving Vue dev warnings active in shipped builds. The CSP regex silently failing on non-canonical attribute order is a secondary concern.

tools/ts-builder/src/configs/utils/createViteDevConfig.ts — the unconditional process.env define and attribute-order-sensitive CSP regex both live here.

Important Files Changed

Filename Overview
tools/ts-builder/src/configs/utils/createViteDevConfig.ts Adds CSP-relaxation plugin and process.env shim; process.env define is not guarded by isServe, potentially affecting production builds
tools/ts-builder/src/commands/utils/dev-server-sidecar.ts New sidecar lifecycle module — signal cleanup logic and exit codes need attention
tools/ts-builder/src/commands/serve.ts Block-UI serve path refactored to programmatic Vite API with sidecar publishing; generally sound
tools/ts-builder/src/commands/utils/index.ts Re-exports new dev-server-sidecar module; trivial barrel change

Sequence Diagram

sequenceDiagram
    participant Author as Block Author (CLI)
    participant TS as ts-builder serve
    participant SC as SidecarLifecycle
    participant Vite as Vite Dev Server
    participant FS as dist/.dev-server
    participant Desktop as Desktop App

    Author->>TS: ts-builder serve --target block-ui
    TS->>SC: setupSidecarLifecycle(sidecarPath)
    Note over SC: registers SIGINT/SIGTERM/SIGHUP/exit/uncaughtException
    TS->>Vite: vite.createServer(configFile, port, host)
    Vite-->>TS: server instance
    TS->>Vite: server.listen()
    Vite-->>TS: resolvedUrls.local[0]
    TS->>SC: "sidecar.publish({schema:1, url, pid})"
    SC->>FS: atomic write (tmp then rename)
    TS->>Vite: patch server.close() to call sidecar.remove()

    Desktop->>FS: read .dev-server JSON
    Desktop->>Vite: HEAD probe URL
    Vite-->>Desktop: 200 OK
    Desktop->>Vite: load block UI (HMR enabled)

    Author->>TS: Ctrl-C (SIGINT)
    TS->>SC: cleanup()
    SC->>FS: fs.unlink(.dev-server)
    TS->>TS: process.exit(0)

    Note over Desktop,FS: Stale sidecar (SIGKILL path): HEAD probe fails, fallback to block-ui:// folder protocol
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
tools/ts-builder/src/configs/utils/createViteDevConfig.ts:141-155
The `"process.env": "({})"` define is applied unconditionally — including when `command === "build"` and `mode === "production"`. In a production build, Vite internally sets `"process.env.NODE_ENV"` to `JSON.stringify(mode)`, but the broader `"process.env"` substitution replaces the entire object with `({})` before more-specific keys can win, making `process.env.NODE_ENV` resolve to `undefined`. Libraries like Vue use `process.env.NODE_ENV === "production"` for dead-code elimination; leaving it `undefined` can produce larger, non-optimised production bundles with dev warnings active. Guard this define with `isServe` so it only fires during `vite dev`.

```suggestion
    define: {
      "import.meta.vitest": "undefined",
      // `vite dev` does not strip `process.env.X` references the way the
      // production rolldown build does. Transitive imports (e.g.
      // `pf-spec-driver/logging.ts`, `pf-driver/logging.ts`) hit top-level
      // `process.env.MI_LOG_PFRAMES` lookups and throw
      // `ReferenceError: process is not defined` in the browser at module
      // load, leaving the block view white.
      //
      // Inlining an empty object makes every `process.env.X` resolve to
      // `undefined`. Side effect: `process.env.NODE_ENV` is also `undefined`
      // in dev — fine for our current consumers, but flagged in the
      // top-of-file follow-up block as a thing to tighten later.
      ...(isServe ? { "process.env": "({})" } : {}),
    },
```

### Issue 2 of 4
tools/ts-builder/src/commands/utils/dev-server-sidecar.ts:57-64
The signal handlers always call `process.exit(0)`, making SIGINT/SIGTERM look like a clean success to any parent process or shell. The conventional exit codes are 130 for SIGINT, 143 for SIGTERM, and 129 for SIGHUP (128 + signal number). pnpm scripts, CI systems, and process supervisors that inspect `$?` may mis-classify the termination. The idiomatic fix is to remove the handler and re-kill the process with the original signal, letting the kernel apply the default disposition.

```suggestion
  const exitOn = (signal: NodeJS.Signals) => {
    const handler = () => {
      cleanup();
      // Re-raise the signal with the kernel's default disposition so the
      // shell / pnpm sees the conventional exit status (130 for SIGINT,
      // 143 for SIGTERM, 129 for SIGHUP).
      process.removeListener(signal, handler);
      process.kill(process.pid, signal);
    };
    process.on(signal, handler);
  };
```

### Issue 3 of 4
tools/ts-builder/src/configs/utils/createViteDevConfig.ts:99-100
The CSP regex matches only when `http-equiv` appears before `content`. HTML attributes can appear in any order; if a block's `index.html` writes `<meta content="…" http-equiv="Content-Security-Policy">`, the regex silently misses the tag and WASM fails to load in dev without any warning.

```suggestion
      // Match regardless of attribute order (http-equiv before or after content).
      const cspMetaRegex =
        /<meta\b[^>]*\bhttp-equiv="Content-Security-Policy"[^>]*\bcontent="([^"]*)"[^>]*\/?>/i;
```

### Issue 4 of 4
tools/ts-builder/src/commands/serve.ts:108-111
**Server left running without sidecar on missing URL**

When `server.resolvedUrls?.local?.[0]` is undefined, the function returns early but the Vite server remains listening. The patched `server.close` that removes the sidecar is never installed, so if a test harness calls `server.close()` directly the server shuts down but cleanup wiring is absent. Consider closing the server explicitly and exiting with an error when no URL is reported.

Reviews (1): Last reviewed commit: "fix(ts-builder): register sidecar cleanu..." | Re-trigger Greptile

Greptile also left 4 inline comments on this PR.

@changeset-bot

changeset-bot Bot commented May 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 54036b8

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a dev-server sidecar mechanism for the block-ui target in ts-builder serve. It implements atomic writing and lifecycle cleanup of a .dev-server sidecar JSON file, programmatically spawns Vite, and applies temporary dev-mode security relaxations (relaxing CSP for WASM and mocking process.env to prevent runtime crashes). The review feedback provides valuable improvements to make the sidecar mechanism more robust, including resolving Vite relative to the consumer's configuration path, adding a fallback for retrieving the local server URL, correctly re-raising signals on termination to preserve exit codes, and hardening the CSP regex replacement to handle missing or case-variant script-src directives.

Comment on lines +87 to +94
let vite: typeof import("vite");
try {
vite = await import("vite");
} catch (error) {
console.error("Failed to load `vite`. Ensure it's installed as a dependency.", error);
process.exit(1);
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Resolving vite directly via import("vite") relies on Node's default resolution from the ts-builder installation directory. In monorepos, symlinked environments, or global installations, this can fail or resolve to an incorrect version. It is safer to resolve vite relative to the consumer's configuration file path using createRequire and pathToFileURL.

  let vite: typeof import("vite");
  try {
    const { createRequire } = await import("node:module");
    const { pathToFileURL } = await import("node:url");
    const localRequire = createRequire(opts.configPath);
    vite = await import(pathToFileURL(localRequire.resolve("vite")).href);
  } catch (error) {
    console.error("Failed to load `vite`. Ensure it's installed as a dependency.", error);
    process.exit(1);
    return;
  }

Comment on lines +107 to +111
const url = server.resolvedUrls?.local?.[0];
if (!url) {
console.warn("vite did not report a local URL; skipping dev-server sidecar.");
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In some network environments or containerized setups, server.resolvedUrls might be empty or slow to populate. We can fall back to constructing the URL from the underlying HTTP server's bound address and port to ensure the sidecar is successfully written.

  let url = server.resolvedUrls?.local?.[0];
  if (!url) {
    const address = server.httpServer?.address();
    if (address && typeof address === "object") {
      const host = address.address === "::" || address.address === "0.0.0.0" ? "localhost" : address.address;
      const protocol = server.config.server.https ? "https" : "http";
      url = `${protocol}://${host}:${address.port}`;
    }
  }

  if (!url) {
    console.warn("vite did not report a local URL; skipping dev-server sidecar.");
    return;
  }

Comment on lines +57 to +64
const exitOn = (signal: NodeJS.Signals) => {
process.on(signal, () => {
cleanup();
// Re-raise the default behavior with a clean exit code so callers
// (shell, pnpm) see the expected termination.
process.exit(0);
});
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Exiting with code 0 on signals like SIGINT or SIGTERM hides the interruption from parent processes (such as pnpm, npm, or shell scripts), which can cause chained commands (e.g., ts-builder serve && do-something-else) to continue running even after the user presses Ctrl+C. It is standard practice to clean up, remove the signal listener, and re-raise the signal to let the process terminate naturally with the correct status.

Suggested change
const exitOn = (signal: NodeJS.Signals) => {
process.on(signal, () => {
cleanup();
// Re-raise the default behavior with a clean exit code so callers
// (shell, pnpm) see the expected termination.
process.exit(0);
});
};
const exitOn = (signal: NodeJS.Signals) => {
const handler = () => {
cleanup();
process.removeListener(signal, handler);
process.kill(process.pid, signal);
};
process.on(signal, handler);
};

Comment on lines +103 to +106
const relaxed = content.replace(
/script-src([^;]*)/,
(m, srcs: string) => `script-src${srcs} 'wasm-unsafe-eval'`,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current replacement assumes script-src is always present and is written in lowercase. If script-src is missing (e.g., the CSP only defines default-src), the replacement will fail silently. Additionally, CSP directives are case-insensitive. We should make the regex case-insensitive and provide a fallback to default-src or append a new directive if script-src is not found.

        let relaxed = content;
        if (/script-src/i.test(content)) {
          relaxed = content.replace(
            /script-src([^;]*)/i,
            (m, srcs: string) => `script-src${srcs} 'wasm-unsafe-eval'`,
          );
        } else if (/default-src/i.test(content)) {
          relaxed = content.replace(
            /default-src([^;]*)/i,
            (m, srcs: string) => `default-src${srcs} 'wasm-unsafe-eval'`,
          );
        } else {
          relaxed = content ? `${content}; script-src 'wasm-unsafe-eval'` : "script-src 'wasm-unsafe-eval'";
        }

Comment on lines 141 to 155
define: {
"import.meta.vitest": "undefined",
// `vite dev` does not strip `process.env.X` references the way the
// production rolldown build does. Transitive imports (e.g.
// `pf-spec-driver/logging.ts`, `pf-driver/logging.ts`) hit top-level
// `process.env.MI_LOG_PFRAMES` lookups and throw
// `ReferenceError: process is not defined` in the browser at module
// load, leaving the block view white.
//
// Inlining an empty object makes every `process.env.X` resolve to
// `undefined`. Side effect: `process.env.NODE_ENV` is also `undefined`
// in dev — fine for our current consumers, but flagged in the
// top-of-file follow-up block as a thing to tighten later.
"process.env": "({})",
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 The "process.env": "({})" define is applied unconditionally — including when command === "build" and mode === "production". In a production build, Vite internally sets "process.env.NODE_ENV" to JSON.stringify(mode), but the broader "process.env" substitution replaces the entire object with ({}) before more-specific keys can win, making process.env.NODE_ENV resolve to undefined. Libraries like Vue use process.env.NODE_ENV === "production" for dead-code elimination; leaving it undefined can produce larger, non-optimised production bundles with dev warnings active. Guard this define with isServe so it only fires during vite dev.

Suggested change
define: {
"import.meta.vitest": "undefined",
// `vite dev` does not strip `process.env.X` references the way the
// production rolldown build does. Transitive imports (e.g.
// `pf-spec-driver/logging.ts`, `pf-driver/logging.ts`) hit top-level
// `process.env.MI_LOG_PFRAMES` lookups and throw
// `ReferenceError: process is not defined` in the browser at module
// load, leaving the block view white.
//
// Inlining an empty object makes every `process.env.X` resolve to
// `undefined`. Side effect: `process.env.NODE_ENV` is also `undefined`
// in dev — fine for our current consumers, but flagged in the
// top-of-file follow-up block as a thing to tighten later.
"process.env": "({})",
},
define: {
"import.meta.vitest": "undefined",
// `vite dev` does not strip `process.env.X` references the way the
// production rolldown build does. Transitive imports (e.g.
// `pf-spec-driver/logging.ts`, `pf-driver/logging.ts`) hit top-level
// `process.env.MI_LOG_PFRAMES` lookups and throw
// `ReferenceError: process is not defined` in the browser at module
// load, leaving the block view white.
//
// Inlining an empty object makes every `process.env.X` resolve to
// `undefined`. Side effect: `process.env.NODE_ENV` is also `undefined`
// in dev — fine for our current consumers, but flagged in the
// top-of-file follow-up block as a thing to tighten later.
...(isServe ? { "process.env": "({})" } : {}),
},
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/ts-builder/src/configs/utils/createViteDevConfig.ts
Line: 141-155

Comment:
The `"process.env": "({})"` define is applied unconditionally — including when `command === "build"` and `mode === "production"`. In a production build, Vite internally sets `"process.env.NODE_ENV"` to `JSON.stringify(mode)`, but the broader `"process.env"` substitution replaces the entire object with `({})` before more-specific keys can win, making `process.env.NODE_ENV` resolve to `undefined`. Libraries like Vue use `process.env.NODE_ENV === "production"` for dead-code elimination; leaving it `undefined` can produce larger, non-optimised production bundles with dev warnings active. Guard this define with `isServe` so it only fires during `vite dev`.

```suggestion
    define: {
      "import.meta.vitest": "undefined",
      // `vite dev` does not strip `process.env.X` references the way the
      // production rolldown build does. Transitive imports (e.g.
      // `pf-spec-driver/logging.ts`, `pf-driver/logging.ts`) hit top-level
      // `process.env.MI_LOG_PFRAMES` lookups and throw
      // `ReferenceError: process is not defined` in the browser at module
      // load, leaving the block view white.
      //
      // Inlining an empty object makes every `process.env.X` resolve to
      // `undefined`. Side effect: `process.env.NODE_ENV` is also `undefined`
      // in dev — fine for our current consumers, but flagged in the
      // top-of-file follow-up block as a thing to tighten later.
      ...(isServe ? { "process.env": "({})" } : {}),
    },
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +57 to +64
const exitOn = (signal: NodeJS.Signals) => {
process.on(signal, () => {
cleanup();
// Re-raise the default behavior with a clean exit code so callers
// (shell, pnpm) see the expected termination.
process.exit(0);
});
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The signal handlers always call process.exit(0), making SIGINT/SIGTERM look like a clean success to any parent process or shell. The conventional exit codes are 130 for SIGINT, 143 for SIGTERM, and 129 for SIGHUP (128 + signal number). pnpm scripts, CI systems, and process supervisors that inspect $? may mis-classify the termination. The idiomatic fix is to remove the handler and re-kill the process with the original signal, letting the kernel apply the default disposition.

Suggested change
const exitOn = (signal: NodeJS.Signals) => {
process.on(signal, () => {
cleanup();
// Re-raise the default behavior with a clean exit code so callers
// (shell, pnpm) see the expected termination.
process.exit(0);
});
};
const exitOn = (signal: NodeJS.Signals) => {
const handler = () => {
cleanup();
// Re-raise the signal with the kernel's default disposition so the
// shell / pnpm sees the conventional exit status (130 for SIGINT,
// 143 for SIGTERM, 129 for SIGHUP).
process.removeListener(signal, handler);
process.kill(process.pid, signal);
};
process.on(signal, handler);
};
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/ts-builder/src/commands/utils/dev-server-sidecar.ts
Line: 57-64

Comment:
The signal handlers always call `process.exit(0)`, making SIGINT/SIGTERM look like a clean success to any parent process or shell. The conventional exit codes are 130 for SIGINT, 143 for SIGTERM, and 129 for SIGHUP (128 + signal number). pnpm scripts, CI systems, and process supervisors that inspect `$?` may mis-classify the termination. The idiomatic fix is to remove the handler and re-kill the process with the original signal, letting the kernel apply the default disposition.

```suggestion
  const exitOn = (signal: NodeJS.Signals) => {
    const handler = () => {
      cleanup();
      // Re-raise the signal with the kernel's default disposition so the
      // shell / pnpm sees the conventional exit status (130 for SIGINT,
      // 143 for SIGTERM, 129 for SIGHUP).
      process.removeListener(signal, handler);
      process.kill(process.pid, signal);
    };
    process.on(signal, handler);
  };
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +99 to +100
const cspMetaRegex =
/<meta\s+http-equiv="Content-Security-Policy"\s+content="([^"]*)"\s*\/?>/i;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The CSP regex matches only when http-equiv appears before content. HTML attributes can appear in any order; if a block's index.html writes <meta content="…" http-equiv="Content-Security-Policy">, the regex silently misses the tag and WASM fails to load in dev without any warning.

Suggested change
const cspMetaRegex =
/<meta\s+http-equiv="Content-Security-Policy"\s+content="([^"]*)"\s*\/?>/i;
// Match regardless of attribute order (http-equiv before or after content).
const cspMetaRegex =
/<meta\b[^>]*\bhttp-equiv="Content-Security-Policy"[^>]*\bcontent="([^"]*)"[^>]*\/?>/i;
Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/ts-builder/src/configs/utils/createViteDevConfig.ts
Line: 99-100

Comment:
The CSP regex matches only when `http-equiv` appears before `content`. HTML attributes can appear in any order; if a block's `index.html` writes `<meta content="…" http-equiv="Content-Security-Policy">`, the regex silently misses the tag and WASM fails to load in dev without any warning.

```suggestion
      // Match regardless of attribute order (http-equiv before or after content).
      const cspMetaRegex =
        /<meta\b[^>]*\bhttp-equiv="Content-Security-Policy"[^>]*\bcontent="([^"]*)"[^>]*\/?>/i;
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +108 to +111
if (!url) {
console.warn("vite did not report a local URL; skipping dev-server sidecar.");
return;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Server left running without sidecar on missing URL

When server.resolvedUrls?.local?.[0] is undefined, the function returns early but the Vite server remains listening. The patched server.close that removes the sidecar is never installed, so if a test harness calls server.close() directly the server shuts down but cleanup wiring is absent. Consider closing the server explicitly and exiting with an error when no URL is reported.

Prompt To Fix With AI
This is a comment left during a code review.
Path: tools/ts-builder/src/commands/serve.ts
Line: 108-111

Comment:
**Server left running without sidecar on missing URL**

When `server.resolvedUrls?.local?.[0]` is undefined, the function returns early but the Vite server remains listening. The patched `server.close` that removes the sidecar is never installed, so if a test harness calls `server.close()` directly the server shuts down but cleanup wiring is absent. Consider closing the server explicitly and exiting with an error when no URL is reported.

How can I resolve this? If you propose a fix, please make it concise.

@codecov

codecov Bot commented May 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 53.21%. Comparing base (698963b) to head (54036b8).
⚠️ Report is 1 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1657      +/-   ##
==========================================
+ Coverage   53.07%   53.21%   +0.13%     
==========================================
  Files         270      270              
  Lines       15715    15715              
  Branches     3411     3411              
==========================================
+ Hits         8341     8363      +22     
+ Misses       6260     6235      -25     
- Partials     1114     1117       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

dbolotin added 4 commits May 26, 2026 18:54
…-ui`

Switches the block-ui branch of `ts-builder serve` from a child-process
Vite spawn to a programmatic `createServer` call. After Vite reports its
resolved local URL, write `<cwd>/dist/.dev-server` (a JSON sidecar with
schema, url, pid) so the desktop app can pick up the live dev URL on the
next block load. Cleanup on SIGINT / SIGTERM / exit / programmatic close.

`browser` and `browser-lib` targets keep the existing spawn behavior —
they don't need a sidecar.

Adds `--sidecar-out <path>` for unusual block layouts.
Two `vite dev`-only relaxations so the dev-server-sidecar workflow can
load the block at all over http:

1. `transformIndexHtml` plugin appends `'wasm-unsafe-eval'` to the
   `script-src` in the block's CSP meta tag. Production block UIs are
   served via `block-ui://` which is registered with `bypassCSP: true`,
   so the meta CSP never bites; http origin enforces it normally and
   the in-page WASM driver (PFrameSpec) cannot compile without this.
   CSP merging is by intersection — a response-header CSP cannot
   loosen a meta-tag CSP, so rewriting the HTML at serve time is the
   only mechanism.

2. `define: { "process.env": "({})" }` so transitive imports that
   touch `process.env.X` at module top level (e.g.
   pf-spec-driver/logging.ts) don't throw `process is not defined` in
   the browser. Rolldown strips these in production builds; `vite dev`
   does not.

Both are dev-only (the plugin is `apply: "serve"`, the define is
applied universally but the production rolldown pipeline overrides
it). Documented inline with the migration plan: drop the bypassCSP
hack on `block-ui://`, ship a canonical CSP in block templates that
includes `'wasm-unsafe-eval'`, replace the blunt `process.env`
substitution with a precise allowlist, and do a security review of
the block runtime end-to-end before treating either as steady state.
The first version of the `'wasm-unsafe-eval'` injector excluded both
quote types from the content character class, so the capture group
ended at the first single quote inside `content="script-src 'self'
blob:"`. The replace never fired and the block view still hit
`CompileError` on WebAssembly compile.

CSP source keywords are always single-quoted inside a double-quoted
content attribute. Restrict the regex to that shape — exclude only the
delimiter (`[^"]*`).

Verified end-to-end against `enter-numbers-v3` over `vite dev`:
PFrameSpec / SpecDriver compiles, block renders, HMR confirmed by
live-editing a .vue file with no reload.
Node fires same-signal handlers in registration order. When the cleanup
hooks were registered AFTER `vite.createServer().listen()`, Vite's own
internal handlers (and anyone else hooking signals during init) ran
first and could exit the process before our unlink ran, leaving stale
sidecars behind.

Restructure into a `setupSidecarLifecycle(path)` factory that:

  - Registers SIGINT/SIGTERM/SIGHUP/exit/uncaughtException up-front
    (before Vite has a chance to attach its own handlers).
  - Exposes `publish(payload)` for after the server URL is known.
  - Tracks a `live` flag so signal-triggered cleanup is idempotent and
    a no-op when nothing was written.
  - Still proxies through `server.close` so HMR-driven restarts and
    programmatic shutdowns also unlink.

Verified end-to-end: direct `kill -TERM <pid>` against the node process
running `ts-builder serve` now removes the sidecar reliably.
@dbolotin dbolotin force-pushed the feat/dev-server-sidecar branch from b17e5bd to 54036b8 Compare May 26, 2026 17:19
@AStaroverov

Copy link
Copy Markdown
Collaborator

this is not ts-builder responsibility, it should be in block-tools. ts-builder - don't know nothing about platform

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants