Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 19 additions & 15 deletions apps/desktop/src/wsl/DesktopWslEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,18 @@ const NODE_PTY_PROBE_SCRIPT = (
printf 'nodeVersion:%s\\n' "$(node -p 'process.versions.node' 2>/dev/null)"
printf 'resolvedPath:%s\\n' "$PATH"
cd ${shellQuote(linuxServerDir)} && node <<'NODE' >/dev/null 2>&1
// The server bundle externalizes its deps to node_modules, and the WSL Node
// can't read inside app.asar, so confirm those deps are unpacked on the real
// filesystem before reporting the backend healthy. "effect" is the framework
// every server module imports; resolving it validates the whole node_modules
// tree. Exit 3 marks this distinct from a node-pty problem so the caller can
// report it accurately instead of letting the server crash on
// ERR_MODULE_NOT_FOUND at launch (which, in wsl-only mode, would just fail to
// launch with no fallback).
try { require.resolve("effect"); } catch (_e) { process.exit(3); }
// The WSL Node can't read inside app.asar, so confirm what the server needs is
// unpacked on the real filesystem before reporting the backend healthy. Exit 3
// marks this distinct from a node-pty prebuild problem so the caller can report
// it accurately instead of letting the server crash on ERR_MODULE_NOT_FOUND at
// launch (which, in wsl-only mode, would just fail to launch with no fallback).
//
// The sentinel must be a package the CLI bundle leaves external. It used to be
// "effect", back when the bundle externalized its runtime deps and the whole
// node_modules tree was unpacked. The bundle now inlines its JS dependencies,
// so "effect" no longer exists on disk and only the native packages do —
// resolving node-pty is what actually validates the unpacked tree.
try { require.resolve("node-pty/package.json"); } catch (_e) { process.exit(3); }
const fs = require("node:fs");
const path = require("node:path");
const pkgDir = path.dirname(require.resolve("node-pty/package.json"));
Expand Down Expand Up @@ -462,16 +465,17 @@ const ensureNodePtyImpl = (
} as const;
}

// Server dependencies (e.g. "effect") couldn't be resolved on the WSL
// filesystem — a packaging regression, since the server bundle needs its
// node_modules unpacked from the asar. Fatal so wsl-only mode falls back to
// Windows and dual mode surfaces the reason inline, instead of the server
// crash-looping on ERR_MODULE_NOT_FOUND once it actually launches.
// The packages the server bundle leaves external (node-pty and the other
// native addons) couldn't be resolved on the WSL filesystem — a packaging
// regression, since those must be unpacked from the asar. Fatal so wsl-only
// mode falls back to Windows and dual mode surfaces the reason inline,
// instead of the server crash-looping on ERR_MODULE_NOT_FOUND once it
// actually launches.
Comment thread
cursor[bot] marked this conversation as resolved.
if (probe.exitCode === 3) {
return {
ok: false,
reason:
"WSL server dependencies could not be loaded (for example \"effect\"). The server's bundled node_modules is not readable by the WSL distro's Node — this is a packaging problem with this build. Please report it.",
'WSL server dependencies could not be loaded (for example "node-pty"). The native packages the server needs are not unpacked where the WSL distro\'s Node can read them — this is a packaging problem with this build. Please report it.',
fatal: true,
} as const;
}
Expand Down
29 changes: 20 additions & 9 deletions apps/server/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import baseConfig from "../../vite.config.ts";
import { loadRepoEnv } from "../../scripts/lib/public-config.ts";
import packageJson from "./package.json" with { type: "json" };

const bundledPackagePrefixes = [
"@pierre/diffs",
"@t3tools/",
"effect-acp",
"effect-codex-app-server",
];
// The bundle used to inline only workspace packages, leaving every third-party
// runtime dep external. External deps must exist on the real filesystem (the WSL
// backend runs plain `wsl.exe -- node`, which cannot read inside an asar), so the
// desktop build unpacked `**\/node_modules\/**` wholesale: 13,875 loose files to
// support 20 native binaries. NSIS install time tracks file count, not bytes.
//
// Inverted here — bundle everything except the packages that genuinely cannot be
// inlined. See scripts/lib/cli-external-packages.ts for what earns an exemption.
import {
isExternalCliDependency,
shouldBundleCliDependency,
} from "../../scripts/lib/cli-external-packages.ts";

export function shouldBundleCliDependency(id: string): boolean {
return bundledPackagePrefixes.some((prefix) => id.startsWith(prefix));
}
export { shouldBundleCliDependency };

const repoEnv = loadRepoEnv();
const cliBuildChannel = packageJson.version.includes("-nightly.") ? "nightly" : "latest";
Expand All @@ -37,7 +41,14 @@ export default mergeConfig(
sourcemap: true,
clean: true,
deps: {
// Both halves are required. `alwaysBundle` forces the JS dependencies in
// (declared deps are external by default, which is what this change is
// undoing). `neverBundle` forces the native packages out: returning
// false from `alwaysBundle` only means "no opinion", so a transitive
// dependency would still be bundled — which silently inlined
// msgpackr-extract and its loader, losing native acceleration.
alwaysBundle: shouldBundleCliDependency,
neverBundle: (id: string) => isExternalCliDependency(id),
onlyBundle: false,
},
banner: {
Expand Down
38 changes: 38 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand Down Expand Up @@ -767,3 +768,40 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
}),
);
});

// The self-containment check runs the packaged tree in a scratch directory. Its
// own node_modules holds the unpacked externals and must be ignored, but any
// node_modules *above* it would let Node's parent walk satisfy an import that is
// missing from the package, so the probe refuses to run in that case.
it("lists ancestor node_modules, nearest first, excluding the start directory", () => {
assert.deepStrictEqual(ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"), [
"C:\\tmp\\probe\\node_modules",
"C:\\tmp\\node_modules",
"C:\\node_modules",
]);
});

it("includes the filesystem root for posix paths", () => {
assert.deepStrictEqual(ancestorNodeModulesPaths("/tmp/probe", "/"), [
"/tmp/node_modules",
"/node_modules",
]);
});

// A UNC root must keep its \\server\share prefix. Rebuilding it from segments
// produced relative paths, which fs.exists resolves against the build cwd, so
// the guard checked directories that do not exist and silently passed.
it("keeps the prefix of a UNC path instead of going relative", () => {
const paths = ancestorNodeModulesPaths("\\\\server\\share\\tmp\\app", "\\");
for (const candidate of paths) {
assert.ok(candidate.startsWith("\\\\server\\share"), candidate);
}
assert.deepStrictEqual(paths[0], "\\\\server\\share\\tmp\\node_modules");
});

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading