()
| 287 | * a rebuild, matching how `workerBundlerArtifact` addresses its own artifacts. |
| 288 | */ |
| 289 | export const mcpAppsShellAsset = (): McpAppsShellAssetVitePlugin => { |
| 290 | let command: "build" | "serve" = "build"; |
| 291 | let ssr = false; |
| 292 | let cached: { readonly html: string; readonly url: string } | undefined; |
| 293 | |
| 294 | const load = async (): Promise<{ readonly html: string; readonly url: string }> => { |
| 295 | if (cached) return cached; |
| 296 | const html = await buildMcpAppsShellHtml(); |
| 297 | // Build-blocking: runtime loaders authenticate the document by this |
| 298 | // fragment (an ASSETS-binding miss under SPA not-found handling answers |
| 299 | // with index.html and a 200), so a shell that lost it would make every |
| 300 | // deployed resource read fail. Catch that here, in the build. |
| 301 | if (!html.includes(MCP_APPS_SHELL_DOCUMENT_MARKER)) { |
| 302 | // oxlint-disable-next-line executor/no-try-catch-or-throw, executor/no-error-constructor -- boundary: Vite plugin hooks report build failures by throwing |
| 303 | throw new Error( |
| 304 | `Built MCP-Apps shell is missing its identity marker (${MCP_APPS_SHELL_DOCUMENT_MARKER}); ` + |
| 305 | "check the <meta> in src/shell/mcp-app.html.", |
| 306 | ); |
| 307 | } |
| 308 | const digest = createHash("sha256").update(html).digest("hex").slice(0, 12); |
| 309 | cached = { html, url: `/assets/executor-mcp-apps-shell-${digest}.html` }; |
| 310 | return cached; |
| 311 | }; |
| 312 | |
| 313 | return { |
| 314 | name: "executor-mcp-apps-shell-asset", |
| 315 | enforce: "pre", |
| 316 | configResolved(config) { |
| 317 | command = config.command; |
| 318 | // Recorded per environment, but ONE plugin instance sees every |
| 319 | // environment's `configResolved` before any of their bundles are |
| 320 | // generated — so the last write wins and this cannot be trusted at emit |
| 321 | // time. `generateBundle` reads Rollup's own output options instead; this |
| 322 | // only carries the single-environment case, where the two agree. |
| 323 | ssr = Boolean(config.build?.ssr); |
| 324 | }, |
| 325 | resolveId: (id) => { |
| 326 | if (id === SHELL_VIRTUAL_ID) return SHELL_RESOLVED_ID; |
| 327 | if (id === DEV_HTML_VIRTUAL_ID) return DEV_HTML_RESOLVED_ID; |
| 328 | return undefined; |
| 329 | }, |
| 330 | load: async (id) => { |
| 331 | if (id === DEV_HTML_RESOLVED_ID) { |
| 332 | // The Worker's dev-only escape hatch: under `build` it exports |
| 333 | // `undefined` and `makeAssetsShellHtmlLoader` uses the ASSETS binding; |
| 334 | // under `serve` it carries the built document inline, because no |
| 335 | // assets have been emitted for the binding to find. Guarded the same |
| 336 | // way as the URL module below — the build must not run at startup. |
| 337 | if (command !== "serve") { |
| 338 | return "export const devShellHtml = undefined;\nexport default devShellHtml;\n"; |
| 339 | } |
| 340 | const { html } = await load(); |
| 341 | return `export const devShellHtml = ${JSON.stringify(html)};\nexport default devShellHtml;\n`; |
| 342 | } |
| 343 | if (id !== SHELL_RESOLVED_ID) return undefined; |
| 344 | // Resolving the virtual module is part of Vite's startup graph. Building |
| 345 | // the multi-megabyte shell here makes every dev server pay that cost |
| 346 | // before it can answer even an unrelated route. In serve mode the |
no test coverage detected