Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

it("ignores trailing separators", () => {
assert.deepStrictEqual(
ancestorNodeModulesPaths("C:\\tmp\\probe\\app\\", "\\"),
ancestorNodeModulesPaths("C:\\tmp\\probe\\app", "\\"),
);
});
Loading
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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
86 changes: 86 additions & 0 deletions scripts/build-desktop-artifact.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
import * as NodeServices from "@effect/platform-node/NodeServices";
import { assert, it } from "@effect/vitest";
import * as ConfigProvider from "effect/ConfigProvider";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
Expand DownExpand Up@@ -43,6 +45,8 @@ import {
stageLinuxIconSize,
STAGE_INSTALL_ARGS,
WINDOWS_ASAR_UNPACK,
ancestorNodeModulesPaths,
copyDirectoryPreservingSymlinks,
} from "./build-desktop-artifact.ts";
import { BRAND_ASSET_PATHS } from "./lib/brand-assets.ts";
import { HostProcessArchitecture, HostProcessPlatform } from "@t3tools/shared/hostProcess";
Expand DownExpand Up@@ -767,3 +771,85 @@ 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.effect("rebases packaged links into the isolated tree", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3code-copy-symlinks-" });
const source = path.join(root, "source");
const destination = path.join(root, "destination");
const packageDir = path.join(source, "node_modules/.pnpm/example@1/node_modules/example");
const relativePackageLink = path.join(source, "node_modules/example-relative");
const absolutePackageLink = path.join(source, "node_modules/example-absolute");

yield* fs.makeDirectory(packageDir, { recursive: true });
yield* fs.writeFileString(path.join(packageDir, "index.js"), "module.exports = true;\n");
yield* fs.symlink(
path.join(".pnpm", "example@1", "node_modules", "example"),
relativePackageLink,
);
yield* fs.symlink(packageDir, absolutePackageLink);

yield* copyDirectoryPreservingSymlinks(source, destination);

const copiedPackage = path.join(
destination,
"node_modules/.pnpm/example@1/node_modules/example",
);
const resolvedCopiedPackage = yield* fs.realPath(copiedPackage);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-relative")),
copiedPackage,
);
assert.equal(
yield* fs.readLink(path.join(destination, "node_modules/example-absolute")),
copiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-relative")),
resolvedCopiedPackage,
);
assert.equal(
yield* fs.realPath(path.join(destination, "node_modules/example-absolute")),
resolvedCopiedPackage,
);
}).pipe(Effect.provide(NodeServices.layer)),
);

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