| 1 | /** |
| 2 | * The single source of truth for packages the server CLI bundle must NOT inline. |
| 3 | * |
| 4 | * Two consumers derive from this list, and they must never disagree: |
| 5 | * |
| 6 | * - apps/server/vite.config.ts decides what stays external to the bundle. |
| 7 | * - scripts/build-desktop-artifact.ts selects the runtime dependency roots for |
| 8 | * the Windows server sidecar. |
| 9 | * |
| 10 | * A runtime package that is external but absent from the sidecar fails as soon |
| 11 | * as Node resolves it from the emitted bundle. Keeping both consumers on one |
| 12 | * list prevents packaging from drifting away from the bundle boundary. |
| 13 | * |
| 14 | * Entries are matched as prefixes (`id.startsWith(prefix)`), so they also cover |
| 15 | * a package's platform-specific siblings — `node-gyp-build` covers |
| 16 | * `node-gyp-build-optional-packages`, `@yuuang/` covers every `ffi-rs-*` binding. |
| 17 | */ |
| 18 | /** |
| 19 | * External because Node actually loads them from disk at runtime. |
| 20 | * |
| 21 | * Native addons (.node), the JS wrappers that dlopen them by real path, and — |
| 22 | * critically — the ordinary JS packages those wrappers require. An external |
| 23 | * package is loaded from the real filesystem, so its own `require` also |
| 24 | * resolves from the real filesystem; a dependency that was bundled away exists |
| 25 | * only inside the emitted bundle and is unreachable there. This closure is |
| 26 | * enforced by a test, not by inspection. |
| 27 | */ |
| 28 | export const CLI_RUNTIME_EXTERNAL_PREFIXES = [ |
| 29 | // Cursor ships computed Webpack imports and platform helper packages. |
| 30 | "@cursor/sdk", |
| 31 | // Playwright reads package.json and browsers.json beside its runtime modules. |
| 32 | "playwright-core", |
| 33 | "node-pty", |
| 34 | "ffi-rs", |
| 35 | "@yuuang/", |
| 36 | "@ff-labs/", |
| 37 | "@napi-rs/keyring", |
| 38 | "@clerk/electron-passkeys", |
| 39 | "node-gyp-build", |
| 40 | "node-addon-api", |
| 41 | // ws's optional accelerators. Nothing in this repo declares them, so they are |
| 42 | // not in the staged production install and the packaged app does not ship |
| 43 | // them either way -- ws wraps the require in try/catch and falls back to its |
| 44 | // JS paths. They are listed because they were being inlined from the dev |
| 45 | // store: both carry binding.gyp and prebuilds and load through |
| 46 | // node-gyp-build, and a native loader inlined into a bundle chunk searches |
| 47 | // for prebuilds that cannot be beside it. Listing them keeps that from |
| 48 | // becoming real if either is ever declared as a dependency. |
| 49 | "bufferutil", |
| 50 | "utf-8-validate", |
| 51 | ] as const; |
| 52 | |
| 53 | // These are Cursor's disk-backed dependency closure. Match package boundaries |
| 54 | // so "zod" does not also externalize unrelated packages such as zod-to-json-schema. |
| 55 | const CURSOR_RUNTIME_DEPENDENCIES = [ |
| 56 | "@bufbuild/protobuf", |
| 57 | "@connectrpc/connect", |
| 58 | "@connectrpc/connect-node", |
| 59 | "@connectrpc/connect-web", |
| 60 | "@statsig/js-client", |
| 61 | "@statsig/client-core", |
| 62 | "zod", |
| 63 | "undici", |
| 64 | "@fastify/busboy", |
| 65 | ] as const; |
| 66 | |
| 67 | export function isRuntimeExternalCliDependency(id: string): boolean { |
| 68 | return ( |
| 69 | CLI_RUNTIME_EXTERNAL_PREFIXES.some((prefix) => id.startsWith(prefix)) || |
| 70 | CURSOR_RUNTIME_DEPENDENCIES.some((name) => id === name || id.startsWith(`${name}/`)) |
| 71 | ); |
| 72 | } |
| 73 | |
| 74 | /** |
| 75 | * True when `id` must stay out of the bundle. |
| 76 | * |
| 77 | * This has to be wired to the bundler's `neverBundle`, not just to |
| 78 | * `alwaysBundle`. `alwaysBundle` only forces packages IN — returning false from |
| 79 | * it means "no opinion", and the default then applies: a declared dependency |
| 80 | * stays external, but a transitive one gets bundled. That is how a native |
| 81 | * loader such as node-gyp-build ended up inlined while node-pty (a declared |
| 82 | * dependency) stayed external. |
| 83 | */ |
| 84 | export function isExternalCliDependency(id: string): boolean { |
| 85 | return isRuntimeExternalCliDependency(id); |
| 86 | } |
| 87 | |
| 88 | /** True when the CLI bundle should inline `id` rather than leave it external. */ |
| 89 | export function shouldBundleCliDependency(id: string): boolean { |
| 90 | if (id.startsWith("node:")) return false; |
| 91 | return !isExternalCliDependency(id); |
| 92 | } |
| 93 | |
| 94 | /** Select direct dependency roots whose runtime closure belongs in the sidecar. */ |
| 95 | export function selectCliRuntimeExternalDependencies( |
| 96 | dependencies: Readonly<Record<string, string>>, |
| 97 | ): Record<string, string> { |
| 98 | return Object.fromEntries( |
| 99 | Object.entries(dependencies).filter(([name]) => isRuntimeExternalCliDependency(name)), |
| 100 | ); |
| 101 | } |
| 102 | |
| 103 | /** |
| 104 | * Scan an emitted bundle chunk for runtime-external packages that were inlined. |
| 105 | * |
| 106 | * Configuring the bundler is not the same as checking what it produced. The |
| 107 | * `alwaysBundle` predicate only forces packages IN; returning false from it |
| 108 | * means "no opinion", so a transitive dependency still gets bundled by default. |
| 109 | * A native loader and its helper (node-gyp-build-optional-packages and |
| 110 | * detect-libc, when msgpackr-extract was still a dependency) were inlined that |
| 111 | * way while every list-based test passed, which is why this reads the artifact |
| 112 | * instead. |
| 113 | * |
| 114 | * `regionCount` is reported so the caller can tell "nothing was inlined" apart |
| 115 | * from "the marker format changed and this scan no longer sees anything". |
| 116 | * |
| 117 | * `inlinedPackages` is every package seen in a region, which lets the caller |
| 118 | * check the opposite direction too. Verifying only that externals are absent |
| 119 | * would still pass if the bundler reverted to leaving everything external: the |
| 120 | * scan would see source-file regions, report nothing inlined, and the packaged |
| 121 | * backends would then fail with ERR_MODULE_NOT_FOUND because those packages |
| 122 | * are not in the selected sidecar closure either. |
| 123 | */ |
| 124 | export function findInlinedExternalPackages(source: string): { |
| 125 | readonly regionCount: number; |
| 126 | readonly inlined: ReadonlyArray<string>; |
| 127 | readonly inlinedPackages: ReadonlyArray<string>; |
| 128 | } { |
| 129 | // Rolldown marks each inlined module with a `//#region <path>` comment. |
| 130 | const regionPattern = /\/\/#region\s+(\S+)/g; |
| 131 | const packagePattern = /node_modules\/((?:@[^/\s]+\/)?[^/\s]+)\//g; |
| 132 | |
| 133 | let regionCount = 0; |
| 134 | const inlined = new Set<string>(); |
| 135 | const inlinedPackages = new Set<string>(); |
| 136 | for (const region of source.matchAll(regionPattern)) { |
| 137 | regionCount += 1; |
| 138 | const regionPath = region[1] ?? ""; |
| 139 | for (const candidate of regionPath.matchAll(packagePattern)) { |
| 140 | const name = candidate[1]; |
| 141 | if (name === undefined || name === ".pnpm") continue; |
| 142 | inlinedPackages.add(name); |
| 143 | if (isExternalCliDependency(name)) inlined.add(name); |
| 144 | } |
| 145 | } |
| 146 | |
| 147 | return { |
| 148 | regionCount, |
| 149 | inlined: [...inlined].sort(), |
| 150 | inlinedPackages: [...inlinedPackages].sort(), |
| 151 | }; |
| 152 | } |