From adf66e0ec977ef052ff33118c3ea9abbb557be7f Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 10:27:17 +0200 Subject: [PATCH 1/3] fix(chat): keep the eval-based dynamic import out of client bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Project pages are served with `script-src 'self' 'nonce-...' https://esm.sh`. That has no 'unsafe-eval', so the `new Function("specifier", ...)` in platform/compat/dynamic-import.ts throws EvalError in the browser. Hydration dies before first paint and the page sits on its skeleton loaders forever. Two barrel imports were pulling that server-only helper into the `veryfront/chat` client entry. Both need only `getHostEnv`, but importing the `platform/compat/process.ts` barrel also drags in its `runCommand` re-export from process/command.ts, which imports dynamic-import.ts: chat/index.ts -> chat/stream-watchdog.ts -> agent/streaming/lifecycle/watchdog-compat-adapter.ts -> platform/compat/process.ts -> process/command.ts chat/index.ts -> react/components/chat/agent-card.tsx -> ... -> react/components/chat/missing-renderer-warning.ts -> platform/environment.ts -> platform/compat/process.ts -> process/command.ts Both now import `platform/compat/process/env.ts` directly, which is where getHostEnv is defined and which is already the convention elsewhere (see security/sandbox/deno-sandbox.ts, utils/logger/logger.ts). The `new Function` itself is deliberately left alone. It is load-bearing on the server: it keeps the import non-literal so neither `deno compile` nor the release-asset rewriter traces into the specifier. Removing it is not an option — the release-asset builder then rejects the module outright with "Release module contains a non-literal dynamic import". The fix is to keep the eval and keep it off the client. Two regression guards, both confirmed red before the fix and green after: - scaffolded-project-build asserts no *published* JS asset contains `new Function`. This is the ground truth: before the fix, 5 of 7 scaffolds published chunk bfbed417dd16 — byte-identical to the asset failing in production — and every existing assertion in that file still passed. - dynamic-import.test walks value imports (not `import type`) from the six client entries in PLATFORM_UTILITY_PATHS and fails with the offending chain. This one found the second chain above, which manual tracing had missed. Verified: uploads per scaffold drop 269 -> 263, so the whole server-only subtree leaves the client bundle rather than just the one chunk. --- .../lifecycle/watchdog-compat-adapter.ts | 8 +- src/platform/compat/dynamic-import.test.ts | 116 ++++++++++++++++++ src/platform/environment.ts | 8 +- .../scaffolded-project-build.test.ts | 23 ++++ 4 files changed, 153 insertions(+), 2 deletions(-) diff --git a/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts b/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts index 3fb7f158cc..2473561140 100644 --- a/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts +++ b/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts @@ -1,5 +1,11 @@ import type { ChatUiMessageChunk, MessageMetadata } from "#veryfront/chat/types.ts"; -import { getHostEnv } from "#veryfront/platform/compat/process.ts"; +// Import from process/env.ts, not the process.ts barrel. This module is +// reachable from the `veryfront/chat` client entry, and the barrel also +// re-exports runCommand from process/command.ts, which pulls +// platform/compat/dynamic-import.ts and its `new Function` into the client +// bundle. Project pages ship a CSP without 'unsafe-eval', so that throws +// EvalError and kills hydration before first paint. +import { getHostEnv } from "#veryfront/platform/compat/process/env.ts"; import { type AbsoluteDeadlineTimer, createAbsoluteDeadline } from "./deadlines.ts"; import type { StreamLifecyclePhase } from "./types.ts"; diff --git a/src/platform/compat/dynamic-import.test.ts b/src/platform/compat/dynamic-import.test.ts index 5383cc8268..fb4ba81152 100644 --- a/src/platform/compat/dynamic-import.test.ts +++ b/src/platform/compat/dynamic-import.test.ts @@ -19,4 +19,120 @@ describe("platform/compat/dynamic-import", () => { () => dynamicImport("__nonexistent_module_12345__"), ); }); + + /** + * This helper must stay OUT of client bundles. + * + * Its `new Function` is load-bearing on the server: it keeps the import + * non-literal so neither `deno compile` nor the release-asset rewriter + * traces into the specifier. But project pages ship + * `script-src 'self' 'nonce-...' https://esm.sh` with no 'unsafe-eval', so + * reaching it from a client entry throws EvalError and kills hydration + * before first paint. + * + * Removing the `new Function` is NOT the fix — the release-asset builder + * then rejects the module with "Release module contains a non-literal + * dynamic import". Keep the eval, keep it off the client. + */ + describe("client bundle reachability", () => { + const SRC_ROOT = new URL("../../", import.meta.url); + + /** Client entries behind PLATFORM_UTILITY_PATHS (src/html/utils.ts). */ + const CLIENT_ENTRIES = [ + "react/runtime/core.ts", + "react/fonts/index.ts", + "chat/index.ts", + "markdown/index.ts", + "mdx/index.ts", + "workflow/react/index.ts", + ]; + + function read(relPath: string): string | null { + try { + return Deno.readTextFileSync(new URL(relPath, SRC_ROOT)); + } catch { + return null; + } + } + + function resolveSpecifier(specifier: string, fromRel: string): string | null { + let base: string; + if (specifier.startsWith("#veryfront/")) { + base = specifier.slice("#veryfront/".length); + } else if (specifier.startsWith(".")) { + const dir = fromRel.includes("/") ? fromRel.slice(0, fromRel.lastIndexOf("/")) : ""; + base = new URL(specifier, `file:///${dir}/`).pathname.replace(/^\/+/, ""); + } else { + return null; + } + const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`]; + for (const candidate of candidates) { + if (read(candidate) !== null) return candidate; + } + return null; + } + + /** Value imports only — `import type` is erased and never ships. */ + function valueImports(source: string): string[] { + const specifiers: string[] = []; + const re = /(?:^|\n)\s*(?:import|export)(\s+type)?\s*([\s\S]*?)from\s*["']([^"']+)["']/g; + let match: RegExpExecArray | null; + while ((match = re.exec(source)) !== null) { + if (match[1]) continue; + const names = (match[2] ?? "").replace(/[{}]/g, "").split(",").map((n) => n.trim()) + .filter(Boolean); + if (names.length > 0 && names.every((n) => n.startsWith("type "))) continue; + specifiers.push(match[3]); + } + const bare = /(?:^|\n)\s*import\s*["']([^"']+)["']/g; + while ((match = bare.exec(source)) !== null) specifiers.push(match[1]); + return specifiers; + } + + const TARGET = "platform/compat/dynamic-import.ts"; + + for (const entry of CLIENT_ENTRIES) { + it(`should not be reachable from ${entry}`, () => { + const parent = new Map([[entry, null]]); + const queue = [entry]; + let found: string | null = null; + + while (queue.length > 0) { + const current = queue.shift()!; + if (current === TARGET) { + found = current; + break; + } + const source = read(current); + if (source === null) continue; + for (const specifier of valueImports(source)) { + const resolved = resolveSpecifier(specifier, current); + if (resolved !== null && !parent.has(resolved)) { + parent.set(resolved, current); + queue.push(resolved); + } + } + } + + let chain = ""; + if (found !== null) { + const path: string[] = []; + let cursor: string | null = found; + while (cursor !== null) { + path.unshift(cursor); + cursor = parent.get(cursor) ?? null; + } + chain = `\n ${path.join("\n -> ")}`; + } + + assertEquals( + found, + null, + `client entry ${entry} value-imports dynamic-import.ts, whose new Function ` + + `the page CSP blocks at runtime — hydration dies before first paint.` + + `\nUsually a barrel import; import the defining module directly instead.${chain}`, + ); + }); + } + }); }); diff --git a/src/platform/environment.ts b/src/platform/environment.ts index 20f7eb9697..7837d50d3f 100644 --- a/src/platform/environment.ts +++ b/src/platform/environment.ts @@ -8,7 +8,13 @@ * @module platform/environment */ -import { getHostEnv } from "#veryfront/platform/compat/process.ts"; +// Import from process/env.ts, not the process.ts barrel. This module is +// reachable from the `veryfront/chat` client entry, and the barrel also +// re-exports runCommand from process/command.ts, which pulls +// platform/compat/dynamic-import.ts and its `new Function` into the client +// bundle. Project pages ship a CSP without 'unsafe-eval', so that throws +// EvalError and kills hydration before first paint. +import { getHostEnv } from "#veryfront/platform/compat/process/env.ts"; export type Environment = "development" | "production" | "test"; diff --git a/src/release-assets/scaffolded-project-build.test.ts b/src/release-assets/scaffolded-project-build.test.ts index f589035202..6ed99c9bce 100644 --- a/src/release-assets/scaffolded-project-build.test.ts +++ b/src/release-assets/scaffolded-project-build.test.ts @@ -260,6 +260,29 @@ describe("release assets: scaffolded project build", () => { assertEquals(result.state, "ready"); assertEquals(rec.states.map(({ state }) => state), []); + // Project pages are served with `script-src 'self' 'nonce-...' + // https://esm.sh` — no 'unsafe-eval'. A published JS asset containing + // `new Function` throws EvalError in the browser and kills hydration + // before first paint, leaving the page on its skeleton loaders. + // + // This asserts the shipped bytes, not the import graph, because the + // build succeeds either way: before the fix these scaffolds published + // platform/compat/dynamic-import.ts as its own chunk and every test here + // still passed. + const evalOffenders = rec.uploads + .filter((upload) => upload.contentType.includes("javascript")) + .filter((upload) => /\bnew Function\s*\(/.test(new TextDecoder().decode(upload.bytes))); + assertEquals( + evalOffenders.map((upload) => upload.hash), + [], + `${templateName} published JS assets containing new Function, which the page CSP ` + + `blocks at runtime: ${ + evalOffenders + .map((upload) => new TextDecoder().decode(upload.bytes).slice(0, 120)) + .join(" | ") + }`, + ); + const manifest = parseReleaseAssetManifest(rec.manifest); assertExists(manifest); assertEquals( From b5aa8ca9ba76b58f56b06cd34927e57580053e4a Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 10:45:17 +0200 Subject: [PATCH 2/3] fix(chat): satisfy lint:test-typecheck and refresh generated API reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two `ci (lint)` tasks, both caught locally against the same chain CI runs: - lint:test-typecheck — regex capture groups are `string | undefined` under the repo's strict indexing, so guard the pushes instead of indexing raw. - docs:api-reference:check — the explanatory comment added to watchdog-compat-adapter.ts shifts its line numbers, and the generated reference pins source links by line. Regenerated with `deno task docs`; the chat.md diff is line-number movement only, no API surface change. --- docs/api-reference/veryfront/chat.md | 22 +++++++++++----------- src/platform/compat/dynamic-import.test.ts | 8 ++++++-- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/docs/api-reference/veryfront/chat.md b/docs/api-reference/veryfront/chat.md index e5904a3958..a26cbd9faf 100644 --- a/docs/api-reference/veryfront/chat.md +++ b/docs/api-reference/veryfront/chat.md @@ -177,8 +177,8 @@ Result returned from use agent. | `ConversationScrollButton` | Render conversation scroll button. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/empty-state.tsx#L116) | | `ConversationsProvider` | ConversationsProvider - calls `useConversations` once with your `store` / `id` / `onSelect` and shares it via `ConversationsContext`. Declare persistence + router wiring here, once, at the app layout; children read it with `useConversationsContext`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/contexts/conversations-context.tsx#L58) | | `CopyButton` | | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/ui/code-block.tsx#L191) | -| `DEFAULT_CHAT_STREAM_IDLE_TIMEOUT_MS` | Default value for chat stream idle timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L7) | -| `DEFAULT_CHAT_STREAM_TOOL_RUNNING_TIMEOUT_MS` | Default value for chat stream tool running timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L9) | +| `DEFAULT_CHAT_STREAM_IDLE_TIMEOUT_MS` | Default value for chat stream idle timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L13) | +| `DEFAULT_CHAT_STREAM_TOOL_RUNNING_TIMEOUT_MS` | Default value for chat stream tool running timeout ms. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L15) | | `DropZoneOverlay` | Drag overlay shown over the composer while files are dragged onto it - the glyph-in-a-circle + "Drop files" from Studio's `PromptForm`. Rendered inside a `relative` card; fills it and blurs the content behind. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/drop-zone.tsx#L18) | | `ErrorBanner` | Render error banner. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/composition/error-banner.tsx#L28) | | `FadeIn` | Render fade in. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/animations.tsx#L37) | @@ -215,19 +215,19 @@ Result returned from use agent. | ------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `agentsToPickerOptions` | Narrow browser-safe agent metadata to the picker's row shape. `AgentOption` now shares `AgentMetadata`'s `avatarUrl` field, so `AgentMetadata[]` is also accepted by `` directly - this helper just drops the fields the rows don't use. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat-agent-picker.tsx#L32) | | `buildChatStreamChunkMessageMetadata` | Builds chat stream chunk message metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L308) | -| `createChatStreamWatchdog` | Create chat stream watchdog backed by the lifecycle deadline primitive. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L280) | -| `createChatStreamWatchdogState` | State for create chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L67) | +| `createChatStreamWatchdog` | Create chat stream watchdog backed by the lifecycle deadline primitive. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L286) | +| `createChatStreamWatchdogState` | State for create chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L73) | | `dedupeChatUiMessageChunks` | Dedupe chat UI message chunks. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L363) | | `downloadMarkdown` | Download messages as a .md file. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/export.ts#L64) | | `exportAsMarkdown` | Convert chat messages to a markdown string. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/export.ts#L12) | | `extractChatMessageMetadata` | Extract chat message metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/chat-ui-message-helpers.ts#L302) | | `extractSourcesFromParts` | Extract sources from native citations and tool result parts. Native source parts map directly, while tool outputs may expose a `documents` array. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/message-parts.ts#L164) | | `getAgentPromptSuggestions` | Return prompt text suggestions that the current Chat component can render. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/react/use-agent-metadata.ts#L161) | -| `getNextChatStreamWatchdogState` | State for get next chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L108) | +| `getNextChatStreamWatchdogState` | State for get next chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L114) | | `getTextContent` | Get text content from chat message parts | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/message-parts.ts#L16) | | `groupPartsInOrder` | Group consecutive parts for ordered rendering Returns an array of groups, each containing either consecutive text parts, a tool part, or a reasoning part | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/message-parts.ts#L102) | -| `isHeartbeatOnlyMetadataChunk` | Check whether a chunk only carries heartbeat metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L198) | -| `isLongRunningToolRunning` | Compatibility helper. Under strict lifecycle deadlines long-running tool names no longer disable the absolute tool-running deadline; in legacy mode they still do. Also exported for callers that classify tool activity. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L94) | +| `isHeartbeatOnlyMetadataChunk` | Check whether a chunk only carries heartbeat metadata. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L204) | +| `isLongRunningToolRunning` | Compatibility helper. Under strict lifecycle deadlines long-running tool names no longer disable the absolute tool-running deadline; in legacy mode they still do. Also exported for callers that classify tool activity. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L100) | | `isReasoningPart` | Check if a part is a reasoning part | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/message-parts.ts#L82) | | `isSkillToolPart` | Check if a tool part is a skill-related tool. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/message-parts.ts#L77) | | `isToolPart` | Check if a part is a tool part | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/utils/message-parts.ts#L32) | @@ -271,7 +271,7 @@ Result returned from use agent. | Name | Description | Source | | ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | `ChatErrorBoundary` | Implement chat error boundary. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/error-boundary.tsx#L17) | -| `ChatStreamIdleTimeoutError` | Error shape for chat stream idle timeout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L49) | +| `ChatStreamIdleTimeoutError` | Error shape for chat stream idle timeout. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L55) | | `ConversationStoreError` | Normalized persistence failure. Store implementations should reject rather than resolve when an operation did not complete; the React hooks wrap custom adapter rejections in this error so consumers can branch on `operation` without parsing an error message. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/persistence/conversation-store.ts#L31) | ### Types @@ -375,9 +375,9 @@ Result returned from use agent. | `ChatSidebarRootProps` | Props accepted by `ChatSidebarRoot`. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/sidebar.types.ts#L49) | | `ChatStepPart` | Public API contract for chat step part. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/protocol.ts#L104) | | `ChatStreamEvent` | Event emitted for chat stream. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/protocol.ts#L221) | -| `ChatStreamWatchdogOptions` | Options accepted by chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L27) | -| `ChatStreamWatchdogPhase` | Public API contract for chat stream watchdog phase. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L12) | -| `ChatStreamWatchdogState` | State for chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L19) | +| `ChatStreamWatchdogOptions` | Options accepted by chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L33) | +| `ChatStreamWatchdogPhase` | Public API contract for chat stream watchdog phase. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L18) | +| `ChatStreamWatchdogState` | State for chat stream watchdog. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/agent/streaming/lifecycle/watchdog-compat-adapter.ts#L25) | | `ChatTab` | Public API contract for chat tab. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/chat/components/tab-switcher.tsx#L13) | | `ChatTextPart` | Chat message part that carries text. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/chat/protocol.ts#L11) | | `ChatTheme` | Public API contract for chat theme. | [source](https://github.com/veryfront/veryfront-code/blob/main/src/react/components/chat/theme.ts#L22) | diff --git a/src/platform/compat/dynamic-import.test.ts b/src/platform/compat/dynamic-import.test.ts index fb4ba81152..74a9af74d5 100644 --- a/src/platform/compat/dynamic-import.test.ts +++ b/src/platform/compat/dynamic-import.test.ts @@ -82,10 +82,14 @@ describe("platform/compat/dynamic-import", () => { const names = (match[2] ?? "").replace(/[{}]/g, "").split(",").map((n) => n.trim()) .filter(Boolean); if (names.length > 0 && names.every((n) => n.startsWith("type "))) continue; - specifiers.push(match[3]); + const specifier = match[3]; + if (specifier !== undefined) specifiers.push(specifier); } const bare = /(?:^|\n)\s*import\s*["']([^"']+)["']/g; - while ((match = bare.exec(source)) !== null) specifiers.push(match[1]); + while ((match = bare.exec(source)) !== null) { + const specifier = match[1]; + if (specifier !== undefined) specifiers.push(specifier); + } return specifiers; } From 1ae9d63429f8ff80b8dadd61678a50e07287a734 Mon Sep 17 00:00:00 2001 From: Kentaro Wakayama Date: Thu, 6 Aug 2026 10:50:24 +0200 Subject: [PATCH 3/3] test(platform): resolve index.tsx directory modules in the reachability guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveSpecifier checked `${base}/index.ts` but not `${base}/index.tsx`, so an extensionless value import resolving through a .tsx directory module returned null and the traversal stopped there — a silent pass. That matters here specifically: the guard walks React subtrees, and src/react/components/chat/chat/index.tsx is a directory module inside chat/index.ts, the one entry that actually leaked. No live blind spot today (nothing currently reaches those three .tsx directory modules through a relative or #veryfront extensionless import), so this closes a false-negative class rather than fixing a miss. Guard still fails on the pre-fix imports. --- src/platform/compat/dynamic-import.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/platform/compat/dynamic-import.test.ts b/src/platform/compat/dynamic-import.test.ts index 74a9af74d5..153cd68deb 100644 --- a/src/platform/compat/dynamic-import.test.ts +++ b/src/platform/compat/dynamic-import.test.ts @@ -65,7 +65,17 @@ describe("platform/compat/dynamic-import", () => { } else { return null; } - const candidates = [base, `${base}.ts`, `${base}.tsx`, `${base}/index.ts`]; + // index.tsx matters as much as index.ts here: the client entries walk + // React subtrees, and src/react/components/chat/chat/index.tsx is a + // directory module inside the one entry that actually leaked. Omitting + // it would make the traversal stop early and silently pass. + const candidates = [ + base, + `${base}.ts`, + `${base}.tsx`, + `${base}/index.ts`, + `${base}/index.tsx`, + ]; for (const candidate of candidates) { if (read(candidate) !== null) return candidate; }