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 */
28export 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.
55const 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
67export 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 */
84export 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. */
89export 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. */
95export 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 */
124export 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}