// Intermediate library build for the embeddable Omnigent island. // // This is a PRE-BUNDLE step, not the final artifact. Vite bundles web and // ALL of its own dependencies (monaco, shiki, xterm, tiptap, react-query, …) // into an ESM entry (`dist-embed/omnigent-embed.js`) plus a `chunks/` tree of // code-split chunks (web's natural `import()` boundaries are PRESERVED, so // Monaco / shiki language grammars / mermaid diagrams stay lazy) and one scoped // stylesheet (`dist-embed/omnigent-embed.css`). The universe monolith then // ingests this graph into its OWN rspack graph (see // `webapp/web/js/genai/omnigent/embed/loadOmnigentEmbed.ts` + the // `@omnigent/embed` alias in `app.rsbuild.config.ts`) and emits the FINAL // hashed/CDN chunks. So Vite owns web's dependency resolution; rspack owns // chunking, hashing, and serving. // // Only React/ReactDOM, react/jsx-runtime, and react-router(-dom) are left as // BARE externals (not bundled, not shimmed). rspack resolves these bare // specifiers to the host monolith's own copies (React 18 / react-router 6.4.1, // matching web), so there is a single React instance and a single // react-router instance shared with the host — no `__OMNIGENT_SHARED__`, no MF. // @tanstack/react-query is BUNDLED (the embed owns its own QueryClient now; see // `embed.tsx`). Standalone (`main.tsx` / `vite.config.ts`) is unaffected. // // The CSS is post-processed to prefix every selector with `.omnigent-app` so // Tailwind's preflight and base resets cannot leak out and clobber the host's // chrome. `:root` / `html` / `body` are remapped onto the scope root itself. import path from "node:path"; import tailwindcss from "@tailwindcss/vite"; import react from "@vitejs/plugin-react"; import postcss from "postcss"; import { defineConfig, type Plugin } from "vite"; const SCOPE = ".omnigent-app"; /** Split a selector list on top-level commas (ignoring commas inside () or []). */ function splitTopLevel(selectorList: string): string[] { const parts: string[] = []; let depth = 0; let current = ""; for (const ch of selectorList) { if (ch === "(" || ch === "[") depth++; else if (ch === ")" || ch === "]") depth = Math.max(0, depth - 1); if (ch === "," && depth === 0) { parts.push(current); current = ""; } else { current += ch; } } if (current.trim() !== "") parts.push(current); return parts; } function prefixSelector(selector: string): string { const s = selector.trim(); if (s === "" || s.startsWith(SCOPE)) return s; // Root-level selectors collapse onto the scope element itself. const rootMatch = s.match(/^(?::root|html|body)\b(.*)$/); if (rootMatch) { return `${SCOPE}${rootMatch[1]}`; } return `${SCOPE} ${s}`; } const scopePlugin = (): postcss.Plugin => ({ postcssPlugin: "scope-omnigent", // Flatten `@layer` so embed rules become UNLAYERED. Tailwind v4 emits its // utilities inside `@layer utilities`, but the host monolith's base rules // (e.g. `body h2`) are unlayered — and in the cascade, unlayered styles ALWAYS // beat layered ones regardless of specificity. That let `body h2` override a // scoped utility like `.omnigent-app .text-[11px]`. Since every embed rule is // already scoped under `.omnigent-app`, we don't need layers for internal // ordering; dropping them makes the embed compete on specificity, which it // wins (`.omnigent-app .util` ≥ host's typical element selectors). AtRule(atRule) { if (atRule.name !== "layer") return; // Statement form `@layer a, b, c;` (no body) — just delete the declaration. if (!atRule.nodes) { atRule.remove(); return; } // Block form `@layer name { … }` — hoist its children in place. atRule.replaceWith(atRule.nodes); }, Rule(rule) { const parent = rule.parent; // Leave @keyframes step selectors (0%, from, to) untouched. if (parent && parent.type === "atrule" && /keyframes$/i.test((parent as postcss.AtRule).name)) { return; } if (rule.selectors.every((sel) => sel.trim().startsWith(SCOPE))) return; rule.selector = splitTopLevel(rule.selector).map(prefixSelector).join(", "); }, }); scopePlugin.postcss = true; function scopeCss(css: string): string { return postcss([scopePlugin()]).process(css, { from: undefined }).css; } // Some of web's bundled CJS deps (e.g. the `use-sync-external-store` shim // pulled in transitively) do `var React = require("react")` at runtime. Because // react/react-dom/react-router(-dom) are ESM EXTERNALS here, rolldown can't // statically rewrite those CJS `require()` calls into the ESM imports — it // routes them through its runtime `__require` helper, which in a browser (no // global `require`) throws: `Calling \`require\` for "react" …`. (It also made // rspack emit a `webpackEmptyContext` because the bare `require` token looks // magic.) // // Fix: rewrite rolldown's `__require` helper so that, for our known externals, // it returns the module that's ALREADY statically ESM-imported at the top of // the bundle (rspack dedupes these to the host monolith's single React / // react-router instance — same identity as the rest of the embed). We inject // stable-named namespace imports and point the helper at them. Anything else // still throws (no other external is `require()`d at runtime). This keeps the // "single shared React" guarantee while making the CJS deps work in the browser. function resolveExternalCjsRequire(externals: readonly string[]): Plugin { // Stable identifiers for the injected namespace imports (minifier-proof). const importName = (spec: string) => `__omnigentExt_${spec.replace(/[^a-zA-Z0-9]/g, "_")}`; // We anchor the patch on rolldown's own user-facing error STRING rather than // on the minifier-shaped `((x) => typeof require…` prefix. That string is // part of rolldown's documented behaviour (it links the user to the // bundling-cjs docs), so it's the most stable thing to key on — if it ever // changes, the locator below returns null, the per-build assertion fires, and // we get a loud, actionable failure instead of a silent runtime break. const HELPER_ERROR_MARKER = "Calling `require` for"; // The helper assignment opens with ` = /* @__PURE__ */ (() =>` and // the IIFE body closes with `})` after the throw. We locate the opener by // scanning back from the marker, and the closer by scanning forward. const HELPER_OPENER = /([A-Za-z_$][\w$]*)\s*=\s*\/\* @__PURE__ \*\/\s*\(\([A-Za-z_$][\w$]*\)\s*=>\s*typeof require[^]*$/; /** * Locate rolldown's `__require` helper assignment around the error marker. * Returns the [start, end) slice covering ` = …(function(){…throw…})` * plus the captured LHS variable name, or null if the shape is unrecognised. */ function locateHelper(code: string): { start: number; end: number; varName: string } | null { const markerIdx = code.indexOf(HELPER_ERROR_MARKER); if (markerIdx === -1) return null; // Opener: the last ` = /* @__PURE__ */ ((x) => typeof require…` before // the marker. Search the prefix and take the final match. const opener = code.slice(0, markerIdx).match(HELPER_OPENER); if (!opener || opener.index === undefined) return null; const start = opener.index; const varName = opener[1]; // Closer: the IIFE ends at the first `})` after the throw's marker. const closeIdx = code.indexOf("})", markerIdx); if (closeIdx === -1) return null; const end = closeIdx + 2; return { start, end, varName }; } // rolldown emits the helper exactly once — in the entry (single-file build) or // in a shared runtime chunk (multi-chunk build). `renderChunk` runs per-chunk, // so most chunks won't contain it; we patch the one that does and assert at // the end that we patched exactly once. let patchedCount = 0; return { name: "resolve-external-cjs-require", enforce: "post", buildStart() { patchedCount = 0; }, renderChunk(code) { const helper = locateHelper(code); if (!helper) return null; patchedCount++; const imports = externals .map((spec) => `import * as ${importName(spec)} from ${JSON.stringify(spec)};`) .join("\n"); const table = externals .map((spec) => `${JSON.stringify(spec)}: ${importName(spec)}`) .join(", "); // Replace the helper with a function that resolves our externals from the // injected ESM namespaces (rspack dedupes those to the host's single React // / react-router). No `require` token survives, so rspack never sees a // magic require (no `webpackEmptyContext`); anything unexpected still // throws the same error rolldown would have. const replacement = `${helper.varName} = function(id) {\n` + `\tconst __omnigentExternals = { ${table} };\n` + `\tif (Object.prototype.hasOwnProperty.call(__omnigentExternals, id)) return __omnigentExternals[id];\n` + `\tthrow Error("Calling \`require\` for \\"" + id + "\\" in an environment that doesn't expose the \`require\` function.");\n` + `}`; const next = imports + "\n" + code.slice(0, helper.start) + replacement + code.slice(helper.end); return { code: next, map: null }; }, generateBundle() { if (patchedCount !== 1) { throw new Error( `resolve-external-cjs-require: expected to patch rolldown's __require helper exactly once, but patched it ${patchedCount} times. ` + `The runtime shim shape or chunking changed and the external CJS require fix would silently break. ` + `Re-check locateHelper() against the current rolldown output.`, ); } }, }; } function scopeOmnigentCss(): Plugin { return { name: "scope-omnigent-css", enforce: "post", generateBundle(_options, bundle) { for (const file of Object.values(bundle)) { if (file.type === "asset" && file.fileName.endsWith(".css")) { const css = typeof file.source === "string" ? file.source : Buffer.from(file.source).toString("utf8"); file.source = scopeCss(css); } } }, }; } // Bare externals: web's React plumbing is NOT bundled. The host monolith's // rspack resolves these specifiers to its own copies, so the embed shares the // host's single React + react-router instance. Kept as bare strings (no shims, // no globals) — rspack does the resolution at its build time. const SHARED_EXTERNALS = [ "react", "react-dom", "react/jsx-runtime", "react-router", "react-router-dom", ]; export default defineConfig({ // `base: "./"` makes Vite reference the emitted Monaco worker via a RELATIVE // `new URL("./", import.meta.url)` (instead of an absolute // `/assets/...` string). Monaco is omnigent_ui's own dep, so Vite builds the // worker; the relative `new URL` then survives into the intermediate as a // module reference the monolith's rspack can resolve, re-emit, and // content-hash on its own CDN (see the `@omnigent/embed` wiring in // app.rsbuild.config.ts). rspack owns the FINAL hashed worker name. base: "./", plugins: [ react(), tailwindcss(), scopeOmnigentCss(), resolveExternalCjsRequire(SHARED_EXTERNALS), ], resolve: { alias: { "@": path.resolve(__dirname, "./src"), }, }, // React reads process.env.NODE_ENV; replace at build time so the bundle // runs in a host without a `process` global. define: { "process.env.NODE_ENV": JSON.stringify("production"), }, build: { outDir: path.resolve(__dirname, "./dist-embed"), emptyOutDir: true, cssCodeSplit: false, sourcemap: false, lib: { entry: path.resolve(__dirname, "./src/embed.tsx"), formats: ["es"], }, rollupOptions: { // Bare externals — see SHARED_EXTERNALS. rspack resolves them to the // host's React/react-router; everything else is bundled by Vite here. external: SHARED_EXTERNALS, output: { // PRESERVE Vite's natural code-splitting: web already lazy-loads its // heaviest deps (`lazy(() => import("./MonacoCodeEditor"))`, shiki // language grammars, mermaid diagrams …). We DON'T inline those — Vite // emits the entry plus a `chunks/` tree of dynamic-import chunks, and the // monolith's rspack follows those `./chunks/*` imports into ITS OWN graph // and re-chunks/hashes/CDN-serves them (the entry is the only eager load; // Monaco etc. stay lazy). Stable, unhashed names here — rspack does the // FINAL content-hashing downstream. The Monaco worker + wasm assets are // emitted under `assets/` and re-emitted by rspack. entryFileNames: "omnigent-embed.js", chunkFileNames: "chunks/[name]-[hash].js", assetFileNames: (assetInfo) => { const name = assetInfo.names?.[0] ?? ""; return name.endsWith(".css") ? "omnigent-embed.css" : "assets/[name].[ext]"; }, }, }, }, });