diff --git a/.github/workflows/code-build-test.yml b/.github/workflows/code-build-test.yml new file mode 100644 index 0000000000..637aecb219 --- /dev/null +++ b/.github/workflows/code-build-test.yml @@ -0,0 +1,285 @@ +name: Code Build Test + +# Full build + code signing + verification of apps/code on every push to this +# branch (and on demand), producing downloadable artifacts for inspection. +# +# This intentionally does NOT create or publish any GitHub release — it exists +# to prove the packaging pipeline works end to end before merge. The real +# release pipeline lives in code-release.yml and runs only on v* tags. +# +# Temporary: retarget or delete the branch trigger once this branch is merged. + +on: + push: + branches: + - refactor/electron-vite + workflow_dispatch: + +concurrency: + group: code-build-test-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-macos: + strategy: + fail-fast: false + matrix: + include: + - arch: arm64 + runner: macos-15 + - arch: x64 + runner: macos-15-intel + runs-on: ${{ matrix.runner }} + permissions: + id-token: write + contents: read + env: + NODE_OPTIONS: "--max-old-space-size=8192" + NODE_ENV: production + npm_config_arch: ${{ matrix.arch }} + npm_config_platform: darwin + VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }} + VITE_POSTHOG_API_HOST: ${{ secrets.VITE_POSTHOG_API_HOST }} + POSTHOG_SOURCEMAP_API_KEY: ${{ secrets.POSTHOG_SOURCEMAP_API_KEY }} + POSTHOG_ENV_ID: ${{ secrets.POSTHOG_ENV_ID }} + POSTHOG_HOST: ${{ secrets.POSTHOG_HOST }} + CSC_LINK: ${{ secrets.APPLE_CODESIGN_CERT_BASE64 }} + CSC_KEY_PASSWORD: ${{ secrets.APPLE_CODESIGN_CERT_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@7474bc4690e29a8392af63c5b98e7449536d5c3a # v4.3.1 + with: + role-to-assume: ${{ secrets.AWS_TWIG_APP_ASSETS_ROLE_ARN }} + aws-region: ${{ secrets.AWS_TWIG_APP_ASSETS_REGION }} + mask-aws-account-id: true + unset-current-credentials: true + + - name: Download BerkeleyMono fonts from S3 + run: aws s3 cp s3://${{ secrets.AWS_TWIG_APP_ASSETS_BUCKET }}/fonts/BerkeleyMono/ apps/code/assets/fonts/BerkeleyMono/ --recursive + + - name: Build workspace packages + run: | + pnpm --filter @posthog/electron-trpc run build + pnpm --filter @posthog/platform run build + pnpm --filter @posthog/shared run build + pnpm --filter @posthog/git run build + pnpm --filter @posthog/enricher run build + pnpm --filter @posthog/agent run build + + - name: Build app + working-directory: apps/code + run: | + pnpm exec electron-vite build + if [[ "${{ matrix.arch }}" == "arm64" ]]; then + pnpm exec electron-builder build --mac --arm64 --publish never --config electron-builder.ts + else + pnpm exec electron-builder build --mac --x64 --publish never --config electron-builder.ts + fi + + - name: Verify package + run: | + if [[ "${{ matrix.arch }}" == "arm64" ]]; then + APP_BUNDLE="apps/code/out/mac-arm64/PostHog Code.app" + else + APP_BUNDLE="apps/code/out/mac/PostHog Code.app" + fi + RESOURCES="$APP_BUNDLE/Contents/Resources" + UNPACKED="$RESOURCES/app.asar.unpacked/node_modules" + + if [[ ! -f "$RESOURCES/app-update.yml" ]]; then + echo "FAIL: app-update.yml missing at $RESOURCES/app-update.yml" + exit 1 + fi + echo "OK: app-update.yml" + + for mod in node-pty better-sqlite3 "@parcel/watcher"; do + if [[ ! -d "$UNPACKED/$mod" ]]; then + echo "FAIL: $mod missing in app.asar.unpacked/node_modules" + exit 1 + fi + echo "OK: $mod" + done + + for bin in claude-cli codex-acp; do + if [[ ! -d "$RESOURCES/app.asar.unpacked/.vite/build/$bin" ]]; then + echo "FAIL: $bin missing in bundled binaries" + exit 1 + fi + echo "OK: $bin" + done + + - name: Install Playwright + run: pnpm --filter code exec playwright install + + - name: Smoke test packaged app + env: + CI: true + E2E_APP_ARCH: ${{ matrix.arch }} + run: pnpm --filter code exec playwright test --config=tests/e2e/playwright.config.ts tests/e2e/tests/smoke.spec.ts + + - name: Upload Playwright report + if: failure() + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: build-test-playwright-macos-${{ matrix.arch }} + path: apps/code/playwright-report/ + retention-days: 7 + + - name: Upload artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: code-macos-${{ matrix.arch }} + path: | + apps/code/out/*.dmg + apps/code/out/*-mac.zip + apps/code/out/*.blockmap + apps/code/out/latest-mac.yml + retention-days: 7 + + build-windows: + runs-on: windows-2022 + permissions: + contents: read + env: + NODE_OPTIONS: "--max-old-space-size=8192" + NODE_ENV: production + VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }} + VITE_POSTHOG_API_HOST: ${{ secrets.VITE_POSTHOG_API_HOST }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace packages + run: | + pnpm --filter @posthog/electron-trpc run build + pnpm --filter @posthog/platform run build + pnpm --filter @posthog/shared run build + pnpm --filter @posthog/git run build + pnpm --filter @posthog/enricher run build + pnpm --filter @posthog/agent run build + + - name: Build app + working-directory: apps/code + run: | + pnpm exec electron-vite build + pnpm exec electron-builder build --win --x64 --publish never --config electron-builder.ts + + - name: Upload artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: code-windows + path: | + apps/code/out/*.exe + apps/code/out/latest.yml + apps/code/out/*.blockmap + apps/code/out/squirrel-windows/** + retention-days: 7 + if-no-files-found: ignore + + build-linux: + strategy: + fail-fast: false + matrix: + include: + - runner: ubuntu-24.04 + arch: x64 + - runner: ubuntu-24.04-arm + arch: arm64 + runs-on: ${{ matrix.runner }} + permissions: + contents: read + env: + NODE_OPTIONS: "--max-old-space-size=8192" + NODE_ENV: production + VITE_POSTHOG_API_KEY: ${{ secrets.VITE_POSTHOG_API_KEY }} + VITE_POSTHOG_API_HOST: ${{ secrets.VITE_POSTHOG_API_HOST }} + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Install Linux packaging tooling + # squashfs-tools/zsync/libfuse2t64: AppImage. fakeroot: deb. rpm: rpmbuild. + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + squashfs-tools zsync libfuse2t64 fakeroot rpm + + - name: Setup pnpm + uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4.3.0 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build workspace packages + run: | + pnpm --filter @posthog/electron-trpc run build + pnpm --filter @posthog/platform run build + pnpm --filter @posthog/shared run build + pnpm --filter @posthog/git run build + pnpm --filter @posthog/enricher run build + pnpm --filter @posthog/agent run build + + - name: Build app + working-directory: apps/code + run: | + pnpm exec electron-vite build + if [[ "${{ matrix.arch }}" == "arm64" ]]; then + pnpm exec electron-builder build --linux --arm64 --publish never --config electron-builder.ts + else + pnpm exec electron-builder build --linux --x64 --publish never --config electron-builder.ts + fi + + - name: Upload artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: code-linux-${{ matrix.arch }} + path: | + apps/code/out/*.AppImage + apps/code/out/*.deb + apps/code/out/*.rpm + retention-days: 7 diff --git a/.github/workflows/code-release.yml b/.github/workflows/code-release.yml index f267017508..aef053163d 100644 --- a/.github/workflows/code-release.yml +++ b/.github/workflows/code-release.yml @@ -148,11 +148,11 @@ jobs: APP_VERSION: ${{ steps.version.outputs.version }} working-directory: apps/code run: | - node scripts/build.mjs + pnpm exec electron-vite build if [[ "${{ matrix.arch }}" == "arm64" ]]; then - pnpm exec electron-builder build --mac --arm64 --publish never --config electron-builder.config.cjs + pnpm exec electron-builder build --mac --arm64 --publish never --config electron-builder.ts else - pnpm exec electron-builder build --mac --x64 --publish never --config electron-builder.config.cjs + pnpm exec electron-builder build --mac --x64 --publish never --config electron-builder.ts fi - name: Verify package @@ -302,8 +302,8 @@ jobs: APP_VERSION: ${{ steps.version.outputs.version }} working-directory: apps/code run: | - node scripts/build.mjs - pnpm exec electron-builder build --win --x64 --publish never --config electron-builder.config.cjs + pnpm exec electron-vite build + pnpm exec electron-builder build --win --x64 --publish never --config electron-builder.ts - name: Upload release artifacts shell: pwsh @@ -418,11 +418,11 @@ jobs: APP_VERSION: ${{ steps.version.outputs.version }} working-directory: apps/code run: | - node scripts/build.mjs + pnpm exec electron-vite build if [[ "${{ matrix.arch }}" == "arm64" ]]; then - pnpm exec electron-builder build --linux --arm64 --publish never --config electron-builder.config.cjs + pnpm exec electron-builder build --linux --arm64 --publish never --config electron-builder.ts else - pnpm exec electron-builder build --linux --x64 --publish never --config electron-builder.config.cjs + pnpm exec electron-builder build --linux --x64 --publish never --config electron-builder.ts fi - name: Upload release artifacts diff --git a/apps/code/electron-builder.config.cjs b/apps/code/electron-builder.ts similarity index 66% rename from apps/code/electron-builder.config.cjs rename to apps/code/electron-builder.ts index 0271b96121..f16521b173 100644 --- a/apps/code/electron-builder.config.cjs +++ b/apps/code/electron-builder.ts @@ -1,10 +1,14 @@ -"use strict"; +import { createRequire } from "node:module"; +import type { Configuration } from "electron-builder"; +import { asarUnpackGlobs, packagedFileGlobs } from "./runtime-dependencies"; +import beforePack from "./scripts/before-pack"; + +const require = createRequire(import.meta.url); const skipNotarize = process.env.SKIP_NOTARIZE === "1" || !process.env.APPLE_TEAM_ID; -/** @type {import('electron-builder').Configuration} */ -module.exports = { +const config: Configuration = { appId: "com.posthog.array", productName: "PostHog Code", executableName: "PostHog Code", @@ -19,31 +23,22 @@ module.exports = { nodeGypRebuild: false, generateUpdatesFilesForAllChannels: true, - beforePack: "./scripts/before-pack.cjs", + // English-only product: drop the ~50 other Electron locales (~50 MB). + electronLanguages: ["en", "en-US"], + + beforePack, files: [ ".vite/build/**/*", ".vite/renderer/**/*", "package.json", "!node_modules/**/*", - "node_modules/node-pty/**/*", - "node_modules/node-addon-api/**/*", - "node_modules/@parcel/**/*", - "node_modules/better-sqlite3/**/*", - "node_modules/bindings/**/*", - "node_modules/file-uri-to-path/**/*", - "node_modules/file-icon/**/*", - "node_modules/p-map/**/*", - "node_modules/prebuild-install/**/*", - "node_modules/micromatch/**/*", - "node_modules/is-glob/**/*", - "node_modules/detect-libc/**/*", - "node_modules/braces/**/*", - "node_modules/picomatch/**/*", - "node_modules/is-extglob/**/*", - "node_modules/fill-range/**/*", - "node_modules/to-regex-range/**/*", - "node_modules/is-number/**/*", + ...packagedFileGlobs, + // Sourcemaps are uploaded to PostHog at build time, not consumed in the app. + "!**/*.map", + // better-sqlite3 ships its C amalgamation sources; only the built .node runs. + "!node_modules/better-sqlite3/deps/**", + "!node_modules/better-sqlite3/src/**", ], asarUnpack: [ @@ -53,12 +48,7 @@ module.exports = { ".vite/build/plugins/posthog/**", ".vite/build/codex-acp/**", ".vite/build/grammars/**", - "node_modules/node-pty/**", - "node_modules/@parcel/**", - "node_modules/file-icon/**", - "node_modules/better-sqlite3/**", - "node_modules/bindings/**", - "node_modules/file-uri-to-path/**", + ...asarUnpackGlobs, ], extraResources: [ @@ -86,7 +76,7 @@ module.exports = { extendInfo: { CFBundleIconName: "Icon", }, - notarize: skipNotarize ? false : { teamId: process.env.APPLE_TEAM_ID }, + notarize: !skipNotarize, }, dmg: { @@ -106,7 +96,9 @@ module.exports = { target: ["nsis", "squirrel"], // biome-ignore lint/suspicious/noTemplateCurlyInString: electron-builder interpolation tokens, not JS template literals artifactName: "PostHog-Code-${version}-${arch}-win.${ext}", - icon: "build/app-icon.ico", + // electron-builder generates the multi-size .ico from this 1024px PNG; a real + // .ico must be >=256px and the committed app-icon.ico is only 32px. + icon: "build/app-icon.png", }, nsis: { @@ -116,10 +108,15 @@ module.exports = { squirrelWindows: { name: "PostHogCode", + // Squirrel.Windows requires a URL for the install/shortcut icon. + iconUrl: + "https://raw.githubusercontent.com/PostHog/code/main/apps/code/build/app-icon.ico", }, linux: { target: ["AppImage", "deb", "rpm"], + // biome-ignore lint/suspicious/noTemplateCurlyInString: electron-builder interpolation tokens, not JS template literals + artifactName: "PostHog-Code-${version}-${arch}-linux.${ext}", icon: "build/app-icon.png", category: "Development", mimeTypes: ["x-scheme-handler/posthog-code"], @@ -142,3 +139,5 @@ module.exports = { releaseType: "draft", }, }; + +export default config; diff --git a/apps/code/electron.vite.config.ts b/apps/code/electron.vite.config.ts new file mode 100644 index 0000000000..1b387cb6da --- /dev/null +++ b/apps/code/electron.vite.config.ts @@ -0,0 +1,206 @@ +import { readFileSync } from "node:fs"; +import { builtinModules, createRequire } from "node:module"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import tailwindcss from "@tailwindcss/vite"; +import { devtools } from "@tanstack/devtools-vite"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "electron-vite"; +import { loadEnv } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; +import { buildExternals } from "./runtime-dependencies"; +import { + createForceDevModeDefine, + createPosthogPlugin, + mainAliases, + rendererAliases, +} from "./vite.shared.mjs"; +import { + CONTEXT_MILL_ZIP_URL, + copyClaudeExecutable, + copyCodexAcpBinaries, + copyDrizzleMigrations, + copyEnricherGrammars, + copyPosthogPlugin, + fixFilenameCircularRef, + getBuildDate, + getGitCommit, + SKILLS_ZIP_URL, +} from "./vite-main-plugins.mjs"; +import { autoServicesPlugin } from "./vite-plugin-auto-services"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const require = createRequire(import.meta.url); +const pkg = JSON.parse( + readFileSync(path.resolve(__dirname, "package.json"), "utf-8"), +); + +const nodeExternals = [ + ...builtinModules, + ...builtinModules.map((m) => `node:${m}`), +]; + +// Native .node modules can't be bundled — they stay external and resolve from +// the staged node_modules at runtime (see scripts/before-pack.ts). +const nativeModules = buildExternals; + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, path.resolve(__dirname, "../.."), ""); + const isDev = mode === "development"; + + return { + main: { + plugins: [ + tsconfigPaths({ ignoreConfigErrors: true }), + autoServicesPlugin(path.join(__dirname, "src/main/services")), + fixFilenameCircularRef(), + copyClaudeExecutable(), + copyPosthogPlugin(isDev), + copyDrizzleMigrations(), + copyCodexAcpBinaries(), + copyEnricherGrammars(), + createPosthogPlugin(env, "posthog-code-main"), + ].filter(Boolean), + define: { + __BUILD_COMMIT__: JSON.stringify(getGitCommit()), + __BUILD_DATE__: JSON.stringify(getBuildDate()), + "process.env.VITE_POSTHOG_API_KEY": JSON.stringify( + env.VITE_POSTHOG_API_KEY || "", + ), + "process.env.VITE_POSTHOG_API_HOST": JSON.stringify( + env.VITE_POSTHOG_API_HOST || "", + ), + "process.env.VITE_POSTHOG_ACCESS_TOKEN_OVERRIDE": JSON.stringify( + env.VITE_POSTHOG_ACCESS_TOKEN_OVERRIDE || "", + ), + "process.env.SKILLS_ZIP_URL": JSON.stringify(SKILLS_ZIP_URL), + "process.env.CONTEXT_MILL_ZIP_URL": + JSON.stringify(CONTEXT_MILL_ZIP_URL), + ...createForceDevModeDefine(), + }, + resolve: { + alias: mainAliases, + conditions: ["node"], + mainFields: ["module", "jsnext:main", "jsnext"], + }, + cacheDir: ".vite/cache", + build: { + outDir: path.join(__dirname, ".vite/build"), + emptyOutDir: false, + target: "node18", + sourcemap: true, + minify: false, + reportCompressedSize: false, + commonjsOptions: { + transformMixedEsModules: true, + }, + rollupOptions: { + input: { + bootstrap: path.resolve(__dirname, "src/main/bootstrap.ts"), + "workspace-server": require.resolve( + "@posthog/workspace-server/serve", + ), + }, + output: { + format: "cjs", + entryFileNames: "[name].js", + // Flat chunk layout (no chunks/ subdir) so the main code's runtime + // __dirname stays .vite/build, where the spawned workspace-server.js + // child and its shared chunks are resolved from. + chunkFileNames: "[name]-[hash].js", + }, + external: [ + "electron", + "electron/main", + ...nodeExternals, + ...nativeModules, + ], + onwarn(warning, warn) { + if (warning.code === "UNUSED_EXTERNAL_IMPORT") return; + if ( + warning.code === "EVAL" && + warning.id?.includes("web-tree-sitter") + ) + return; + warn(warning); + }, + }, + }, + }, + + preload: { + plugins: [tsconfigPaths({ ignoreConfigErrors: true })], + resolve: { + conditions: ["node"], + mainFields: ["module", "jsnext:main", "jsnext"], + }, + build: { + outDir: path.join(__dirname, ".vite/build"), + emptyOutDir: false, + sourcemap: true, + rollupOptions: { + input: { preload: path.resolve(__dirname, "src/main/preload.ts") }, + output: { + format: "cjs", + inlineDynamicImports: true, + entryFileNames: "preload.js", + chunkFileNames: "[name].js", + assetFileNames: "[name].[ext]", + }, + external: [ + "electron", + "electron/renderer", + "electron/common", + ...nodeExternals, + ], + }, + }, + }, + + renderer: { + root: __dirname, + plugins: [ + isDev && devtools(), + TanStackRouterVite({ + target: "react", + autoCodeSplitting: true, + routesDirectory: path.resolve( + __dirname, + "../../packages/ui/src/router/routes", + ), + generatedRouteTree: path.resolve( + __dirname, + "../../packages/ui/src/router/routeTree.gen.ts", + ), + }), + tailwindcss(), + react(), + tsconfigPaths({ ignoreConfigErrors: true }), + createPosthogPlugin(env, "posthog-code-renderer"), + ].filter(Boolean), + worker: { + format: "es", + }, + envDir: path.resolve(__dirname, "../.."), + define: { + ...createForceDevModeDefine(), + __APP_VERSION__: JSON.stringify(pkg.version), + }, + resolve: { + alias: rendererAliases, + dedupe: ["react", "react-dom"], + }, + server: { + port: 5173, + }, + build: { + outDir: path.join(__dirname, ".vite/renderer/main_window"), + sourcemap: true, + rollupOptions: { + input: path.resolve(__dirname, "index.html"), + }, + }, + }, + }; +}); diff --git a/apps/code/package.json b/apps/code/package.json index 570dcb973d..be72568873 100644 --- a/apps/code/package.json +++ b/apps/code/package.json @@ -2,6 +2,7 @@ "name": "@posthog/code", "version": "0.0.0-dev", "description": "PostHog Code - desktop task manager", + "homepage": "https://posthog.com", "main": ".vite/build/bootstrap.js", "versionHash": "dynamic", "engines": { @@ -10,14 +11,15 @@ }, "scripts": { "setup": "bash bin/setup", - "dev": "node scripts/dev.mjs", - "start": "node scripts/dev.mjs", - "start:debug": "ELECTRON_INSPECT=5858 node scripts/dev.mjs", - "package": "node scripts/build.mjs && electron-builder build --dir --config electron-builder.config.cjs", - "package:dev": "FORCE_DEV_MODE=1 SKIP_NOTARIZE=1 node scripts/build.mjs && electron-builder build --dir --config electron-builder.config.cjs", - "make": "node scripts/build.mjs && electron-builder build --config electron-builder.config.cjs", + "dev": "electron-vite dev --watch", + "start": "electron-vite dev --watch", + "start:debug": "electron-vite dev --watch --inspect=5858", + "compile": "electron-vite build", + "package": "electron-vite build && electron-builder build --dir --config electron-builder.ts", + "package:dev": "FORCE_DEV_MODE=1 SKIP_NOTARIZE=1 electron-vite build && electron-builder build --dir --config electron-builder.ts", + "make": "electron-vite build && electron-builder build --config electron-builder.ts", "make:linux": "bash scripts/build-linux-docker.sh", - "publish": "node scripts/build.mjs && electron-builder build --publish always --config electron-builder.config.cjs", + "publish": "electron-vite build && electron-builder build --publish always --config electron-builder.ts", "build": "pnpm package", "build-icons": "bash scripts/generate-icns.sh", "typecheck": "tsc -p tsconfig.node.json --noEmit && tsc -p tsconfig.web.json --noEmit", @@ -36,7 +38,7 @@ "tasks", "developer-tools" ], - "author": "PostHog", + "author": "PostHog ", "license": "MIT", "devDependencies": { "@biomejs/biome": "2.2.4", @@ -60,6 +62,7 @@ "adm-zip": "^0.5.16", "electron": "^41.0.0", "electron-builder": "^26.15.3", + "electron-vite": "^4.0.1", "husky": "^9.1.7", "jimp": "^1.6.0", "jsdom": "^26.0.0", diff --git a/apps/code/runtime-dependencies.ts b/apps/code/runtime-dependencies.ts new file mode 100644 index 0000000000..92d62d5ec8 --- /dev/null +++ b/apps/code/runtime-dependencies.ts @@ -0,0 +1,65 @@ +// Single source of truth for native modules (and their runtime-required +// transitive deps). pnpm hoists these to the root node_modules; the packaged +// app needs real copies next to the bundle. +// +// - scripts/before-pack.ts stages them from the hoisted root into the app's +// local node_modules before electron-builder collects files. +// - electron-builder.ts re-includes them (`files`) and unpacks the +// binary-bearing ones from the asar (`asarUnpack`). +// - electron.vite.config.ts marks the native ones external so Vite leaves +// them to be resolved from node_modules at runtime. + +// Staged + packaged on every platform. +export const runtimeNativeModules = [ + "node-pty", + "node-addon-api", + "@parcel/watcher", + "better-sqlite3", + "bindings", + "file-uri-to-path", + "prebuild-install", + "micromatch", + "is-glob", + "detect-libc", + "braces", + "picomatch", + "is-extglob", + "fill-range", + "to-regex-range", + "is-number", +]; + +// file-icon (and its p-map dependency) is only used on macOS. +export const macOnlyNativeModules = ["file-icon", "p-map"]; + +// The subset that ships compiled .node binaries and must be unpacked from asar. +const asarUnpackModules = [ + "node-pty", + "@parcel/watcher", + "file-icon", + "better-sqlite3", + "bindings", + "file-uri-to-path", +]; + +// Modules Vite must not bundle (resolved from the staged node_modules at runtime). +export const buildExternals = [ + "node-pty", + "@parcel/watcher", + "file-icon", + "better-sqlite3", +]; + +// electron-builder ships the whole @parcel scope so the platform-specific +// @parcel/watcher-- staged by before-pack is covered too. +const scopeOf = (name: string) => + name.startsWith("@parcel/") ? "@parcel" : name; + +export const packagedFileGlobs = [ + ...runtimeNativeModules, + ...macOnlyNativeModules, +].map((name) => `node_modules/${scopeOf(name)}/**/*`); + +export const asarUnpackGlobs = asarUnpackModules.map( + (name) => `node_modules/${scopeOf(name)}/**`, +); diff --git a/apps/code/scripts/before-pack.cjs b/apps/code/scripts/before-pack.ts similarity index 75% rename from apps/code/scripts/before-pack.cjs rename to apps/code/scripts/before-pack.ts index dabd24bd4f..9bec1e96c3 100644 --- a/apps/code/scripts/before-pack.cjs +++ b/apps/code/scripts/before-pack.ts @@ -1,12 +1,23 @@ -"use strict"; - -const { cpSync, existsSync, mkdirSync, rmSync } = require("node:fs"); -const path = require("node:path"); +import { cpSync, existsSync, mkdirSync, rmSync } from "node:fs"; +import path from "node:path"; +import { + macOnlyNativeModules, + runtimeNativeModules, +} from "../runtime-dependencies"; const ARCH_X64 = 1; const ARCH_ARM64 = 3; -function copyDep(name, rootNodeModules, localNodeModules) { +type BeforePackContext = { + packager: { platform: { name: string } }; + arch: number; +}; + +function copyDep( + name: string, + rootNodeModules: string, + localNodeModules: string, +): boolean { const src = path.join(rootNodeModules, name); if (!existsSync(src)) { const localSrc = path.join(localNodeModules, name); @@ -31,7 +42,11 @@ function copyDep(name, rootNodeModules, localNodeModules) { return true; } -function copyRequiredDep(name, rootNodeModules, localNodeModules) { +function copyRequiredDep( + name: string, + rootNodeModules: string, + localNodeModules: string, +): void { if (!copyDep(name, rootNodeModules, localNodeModules)) { throw new Error( `[before-pack] required native dependency "${name}" not found in node_modules`, @@ -39,7 +54,7 @@ function copyRequiredDep(name, rootNodeModules, localNodeModules) { } } -module.exports = async function beforePack(context) { +export default async function beforePack(context: BeforePackContext) { const platformName = context.packager.platform.name; const arch = context.arch; @@ -50,26 +65,7 @@ module.exports = async function beforePack(context) { console.log(`[before-pack] root node_modules: ${rootNodeModules}`); console.log(`[before-pack] local node_modules: ${localNodeModules}`); - const commonDeps = [ - "node-pty", - "node-addon-api", - "@parcel/watcher", - "micromatch", - "is-glob", - "detect-libc", - "braces", - "picomatch", - "is-extglob", - "fill-range", - "to-regex-range", - "is-number", - "better-sqlite3", - "bindings", - "file-uri-to-path", - "prebuild-install", - ]; - - for (const dep of commonDeps) { + for (const dep of runtimeNativeModules) { copyDep(dep, rootNodeModules, localNodeModules); } @@ -79,8 +75,9 @@ module.exports = async function beforePack(context) { ? "@parcel/watcher-darwin-x64" : "@parcel/watcher-darwin-arm64"; copyRequiredDep(watcherPkg, rootNodeModules, localNodeModules); - copyDep("file-icon", rootNodeModules, localNodeModules); - copyDep("p-map", rootNodeModules, localNodeModules); + for (const dep of macOnlyNativeModules) { + copyDep(dep, rootNodeModules, localNodeModules); + } } else if (platformName === "win") { const watcherPkg = arch === ARCH_ARM64 @@ -100,4 +97,4 @@ module.exports = async function beforePack(context) { rmSync(watcherBuild, { recursive: true, force: true }); console.log("[before-pack] removed @parcel/watcher/build"); } -}; +} diff --git a/apps/code/scripts/build-linux-docker.sh b/apps/code/scripts/build-linux-docker.sh index 269fe6dd1b..2f5e7d2971 100755 --- a/apps/code/scripts/build-linux-docker.sh +++ b/apps/code/scripts/build-linux-docker.sh @@ -93,11 +93,11 @@ COPYFILE_DISABLE=1 tar -cf - \ pnpm --filter @posthog/enricher build pnpm --filter @posthog/agent build cd apps/code - node scripts/build.mjs + pnpm exec electron-vite build if [ -n "${MAKE_TARGETS:-}" ]; then - pnpm exec electron-builder build --linux $MAKE_TARGETS --${ARCH} --config electron-builder.config.cjs + pnpm exec electron-builder build --linux $MAKE_TARGETS --${ARCH} --config electron-builder.ts else - pnpm exec electron-builder build --linux --${ARCH} --config electron-builder.config.cjs + pnpm exec electron-builder build --linux --${ARCH} --config electron-builder.ts fi mkdir -p /out cp -r out/. /out/ diff --git a/apps/code/scripts/build.mjs b/apps/code/scripts/build.mjs deleted file mode 100644 index 7886cc0191..0000000000 --- a/apps/code/scripts/build.mjs +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env node -import { spawn } from "node:child_process"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const root = path.resolve(__dirname, ".."); - -function runViteBuild(config) { - return new Promise((resolve, reject) => { - const child = spawn("pnpm", ["exec", "vite", "build", "--config", config], { - cwd: root, - stdio: "inherit", - }); - child.on("close", (code) => { - if (code !== 0) { - reject(new Error(`vite build -c ${config} exited with code ${code}`)); - } else { - resolve(); - } - }); - child.on("error", reject); - }); -} - -async function main() { - await runViteBuild("vite.main.config.mts"); - await runViteBuild("vite.preload.config.mts"); - await runViteBuild("vite.workspace-server.config.mts"); - await runViteBuild("vite.renderer.config.mts"); -} - -main().catch((err) => { - console.error(err.message); - process.exit(1); -}); diff --git a/apps/code/scripts/dev.mjs b/apps/code/scripts/dev.mjs deleted file mode 100644 index a224c1bd7c..0000000000 --- a/apps/code/scripts/dev.mjs +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env node -import { spawn } from "node:child_process"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const root = path.resolve(__dirname, ".."); - -const DEV_SERVER_PORT = 5173; - -const children = []; - -function killAll(signal = "SIGTERM") { - for (const child of children) { - if (!child.killed) { - child.kill(signal); - } - } -} - -process.on("SIGINT", () => { - killAll("SIGTERM"); - process.exit(0); -}); -process.on("SIGTERM", () => { - killAll("SIGTERM"); - process.exit(0); -}); - -async function main() { - const rendererServer = spawn( - "pnpm", - [ - "exec", - "vite", - "--config", - "vite.renderer.config.mts", - "--port", - String(DEV_SERVER_PORT), - "--strictPort", - "--mode", - "development", - ], - { - cwd: root, - stdio: ["inherit", "pipe", "pipe"], - }, - ); - children.push(rendererServer); - rendererServer.on("close", (code) => { - killAll("SIGTERM"); - process.exit(code ?? 0); - }); - - let devServerUrl = null; - const watchReady = { main: false, preload: false, ws: false }; - - function isReady() { - return ( - devServerUrl !== null && - watchReady.main && - watchReady.preload && - watchReady.ws - ); - } - - let electronStarted = false; - - function maybeStartElectron() { - if (!isReady() || electronStarted) return; - electronStarted = true; - - const inspectArg = process.env.ELECTRON_INSPECT - ? [`--inspect=${process.env.ELECTRON_INSPECT}`] - : []; - - const electron = spawn( - "pnpm", - ["exec", "electron", ".", "--remote-debugging-port=9222", ...inspectArg], - { - cwd: root, - stdio: "inherit", - env: { - ...process.env, - VITE_DEV_SERVER_URL: devServerUrl, - }, - }, - ); - children.push(electron); - electron.on("close", (code) => { - killAll("SIGTERM"); - process.exit(code ?? 0); - }); - } - - function forwardAndCheck(stream, dest, onLine) { - stream.setEncoding("utf8"); - let buf = ""; - stream.on("data", (chunk) => { - buf += chunk; - let nl = buf.indexOf("\n"); - while (nl !== -1) { - const line = buf.slice(0, nl); - buf = buf.slice(nl + 1); - dest.write(`${line}\n`); - onLine(line); - nl = buf.indexOf("\n"); - } - }); - stream.on("end", () => { - if (buf) { - dest.write(buf); - onLine(buf); - } - }); - } - - forwardAndCheck(rendererServer.stdout, process.stdout, (line) => { - if ( - devServerUrl === null && - line.includes(`localhost:${DEV_SERVER_PORT}`) - ) { - devServerUrl = `http://localhost:${DEV_SERVER_PORT}`; - maybeStartElectron(); - } - }); - forwardAndCheck(rendererServer.stderr, process.stderr, () => {}); - - const builtPattern = /built in|watching for file changes/i; - - function startWatchBuild(config, readyKey) { - const child = spawn( - "pnpm", - [ - "exec", - "vite", - "build", - "--config", - config, - "--watch", - "--mode", - "development", - ], - { - cwd: root, - stdio: ["inherit", "pipe", "pipe"], - }, - ); - children.push(child); - forwardAndCheck(child.stdout, process.stdout, (line) => { - if (!watchReady[readyKey] && builtPattern.test(line)) { - watchReady[readyKey] = true; - maybeStartElectron(); - } - }); - forwardAndCheck(child.stderr, process.stderr, () => {}); - return child; - } - - startWatchBuild("vite.main.config.mts", "main"); - startWatchBuild("vite.preload.config.mts", "preload"); - startWatchBuild("vite.workspace-server.config.mts", "ws"); -} - -main().catch((err) => { - console.error(err.message); - killAll("SIGTERM"); - process.exit(1); -}); diff --git a/apps/code/scripts/postinstall.sh b/apps/code/scripts/postinstall.sh index 255d14f29b..4f12df1a62 100755 --- a/apps/code/scripts/postinstall.sh +++ b/apps/code/scripts/postinstall.sh @@ -12,7 +12,7 @@ SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)" # pnpm skips package-level postinstall scripts when the lockfile is already # satisfied, so if node_modules/electron/dist gets wiped (interrupted download, # cache eviction, arch change, manual cleanup), `pnpm install` won't notice — -# and `node scripts/dev.mjs` then fails with "Electron failed to install +# and `electron-vite dev` then fails with "Electron failed to install # correctly, please delete node_modules/electron and try installing again". # Detect the missing binary and invoke Electron's own install script to fetch it. ELECTRON_DIST="$REPO_ROOT/node_modules/electron/dist" diff --git a/apps/code/src/main/bootstrap.ts b/apps/code/src/main/bootstrap.ts index 424a19792e..6f08d57d6a 100644 --- a/apps/code/src/main/bootstrap.ts +++ b/apps/code/src/main/bootstrap.ts @@ -58,6 +58,13 @@ app.commandLine.appendSwitch("enable-logging", "file"); app.commandLine.appendSwitch("log-file", chromiumLogPath); app.commandLine.appendSwitch("log-level", "0"); +// In dev, expose the renderer over CDP (:9222) for the test-electron-app skill. +// electron-vite launches Electron itself, so this is set in-process rather than +// via a CLI flag. +if (isDev) { + app.commandLine.appendSwitch("remote-debugging-port", "9222"); +} + crashReporter.start({ uploadToServer: false }); // Force IPv4 resolution when "localhost" is used so the agent hits 127.0.0.1 diff --git a/apps/code/src/main/utils/env.ts b/apps/code/src/main/utils/env.ts index 06bc80efea..9b6011e41e 100644 --- a/apps/code/src/main/utils/env.ts +++ b/apps/code/src/main/utils/env.ts @@ -12,7 +12,7 @@ function requireEnv(name: string): string { } /** - * Whether this is a development build (running via node scripts/dev.mjs). + * Whether this is a development build (running via electron-vite dev). * Use this for dev/prod feature gates. Use `isPackaged` from @posthog/platform/app-meta * via DI only when you need ASAR-related behavior (e.g. .unpacked paths). */ diff --git a/apps/code/src/main/window.ts b/apps/code/src/main/window.ts index 312de94655..86f23f043e 100644 --- a/apps/code/src/main/window.ts +++ b/apps/code/src/main/window.ts @@ -23,7 +23,7 @@ import { type WindowStateSchema, windowStateStore } from "./utils/store"; const log = logger.scope("window"); -const MAIN_WINDOW_VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; +const MAIN_WINDOW_VITE_DEV_SERVER_URL = process.env.ELECTRON_RENDERER_URL; const MAIN_WINDOW_VITE_NAME = "main_window"; const __filename = fileURLToPath(import.meta.url); diff --git a/apps/code/vite.main.config.mts b/apps/code/vite-main-plugins.mts similarity index 85% rename from apps/code/vite.main.config.mts rename to apps/code/vite-main-plugins.mts index a8cf0602eb..26430ba2ae 100644 --- a/apps/code/vite.main.config.mts +++ b/apps/code/vite-main-plugins.mts @@ -12,14 +12,12 @@ import { statSync, } from "node:fs"; import { cp, mkdir, readdir, rm, writeFile } from "node:fs/promises"; -import { builtinModules } from "node:module"; import { tmpdir } from "node:os"; import path, { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { unzipSync } from "fflate"; -import { defineConfig, loadEnv, type Plugin } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; +import type { Plugin } from "vite"; // @ts-expect-error - plain ESM helper shared with packages/agent/tsup.config.ts import { CLAUDE_CLI_SUPPORT_DIRS, @@ -29,14 +27,8 @@ import { targetArch, targetPlatform, } from "../../packages/agent/build/native-binary.mjs"; -import { - createForceDevModeDefine, - createPosthogPlugin, - mainAliases, -} from "./vite.shared.mjs"; -import { autoServicesPlugin } from "./vite-plugin-auto-services"; -function getGitCommit(): string { +export function getGitCommit(): string { if (process.env.BUILD_COMMIT) return process.env.BUILD_COMMIT; try { return execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim(); @@ -45,13 +37,13 @@ function getGitCommit(): string { } } -function getBuildDate(): string { +export function getBuildDate(): string { return new Date().toISOString(); } const __dirname = path.dirname(fileURLToPath(import.meta.url)); -function fixFilenameCircularRef(): Plugin { +export function fixFilenameCircularRef(): Plugin { return { name: "fix-filename-circular-ref", enforce: "post", @@ -166,7 +158,7 @@ function copyClaudeSupportAssets(sourcePath: string, destDir: string): void { } } -function copyClaudeExecutable(): Plugin { +export function copyClaudeExecutable(): Plugin { return { name: "copy-claude-executable", writeBundle() { @@ -229,10 +221,10 @@ function getFilesRecursive(dir: string): string[] { return files; } -const SKILLS_ZIP_URL = +export const SKILLS_ZIP_URL = "https://github.com/PostHog/posthog/releases/download/agent-skills-latest/skills.zip"; -const CONTEXT_MILL_ZIP_URL = +export const CONTEXT_MILL_ZIP_URL = "https://github.com/PostHog/context-mill/releases/latest/download/skills-mcp-resources.zip"; const execFileAsync = promisify(execFile); @@ -411,7 +403,7 @@ const PLUGIN_ALLOW_LIST = [ let remoteSkillsFetched = false; -function copyPosthogPlugin(isDev: boolean): Plugin { +export function copyPosthogPlugin(isDev: boolean): Plugin { const sourceDir = join(__dirname, "../../plugins/posthog"); const localSkillsDir = join(sourceDir, "local-skills"); @@ -478,7 +470,7 @@ function copyPosthogPlugin(isDev: boolean): Plugin { }; } -function copyDrizzleMigrations(): Plugin { +export function copyDrizzleMigrations(): Plugin { const migrationsDir = join( __dirname, "../../packages/workspace-server/src/db/migrations", @@ -503,7 +495,7 @@ function copyDrizzleMigrations(): Plugin { let enricherGrammarsCopied = false; -function copyEnricherGrammars(): Plugin { +export function copyEnricherGrammars(): Plugin { return { name: "copy-enricher-grammars", writeBundle() { @@ -549,7 +541,7 @@ function copyEnricherGrammars(): Plugin { let codexAcpCopied = false; -function copyCodexAcpBinaries(): Plugin { +export function copyCodexAcpBinaries(): Plugin { return { name: "copy-codex-acp-binaries", writeBundle() { @@ -601,81 +593,3 @@ function copyCodexAcpBinaries(): Plugin { }, }; } - -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, path.resolve(__dirname, "../.."), ""); - const isDev = mode === "development"; - - return { - plugins: [ - tsconfigPaths({ ignoreConfigErrors: true }), - autoServicesPlugin(join(__dirname, "src/main/services")), - fixFilenameCircularRef(), - copyClaudeExecutable(), - copyPosthogPlugin(isDev), - copyDrizzleMigrations(), - copyCodexAcpBinaries(), - copyEnricherGrammars(), - createPosthogPlugin(env, "posthog-code-main"), - ].filter(Boolean), - define: { - __BUILD_COMMIT__: JSON.stringify(getGitCommit()), - __BUILD_DATE__: JSON.stringify(getBuildDate()), - "process.env.VITE_POSTHOG_API_KEY": JSON.stringify( - env.VITE_POSTHOG_API_KEY || "", - ), - "process.env.VITE_POSTHOG_API_HOST": JSON.stringify( - env.VITE_POSTHOG_API_HOST || "", - ), - "process.env.VITE_POSTHOG_ACCESS_TOKEN_OVERRIDE": JSON.stringify( - env.VITE_POSTHOG_ACCESS_TOKEN_OVERRIDE || "", - ), - "process.env.SKILLS_ZIP_URL": JSON.stringify(SKILLS_ZIP_URL), - "process.env.CONTEXT_MILL_ZIP_URL": JSON.stringify(CONTEXT_MILL_ZIP_URL), - ...createForceDevModeDefine(), - }, - resolve: { - alias: mainAliases, - conditions: ["node"], - mainFields: ["module", "jsnext:main", "jsnext"], - }, - cacheDir: ".vite/cache", - build: { - outDir: path.join(__dirname, ".vite/build"), - emptyOutDir: false, - target: "node18", - sourcemap: true, - minify: false, - reportCompressedSize: false, - commonjsOptions: { - transformMixedEsModules: true, - }, - lib: { - entry: path.resolve(__dirname, "src/main/bootstrap.ts"), - formats: ["cjs"], - fileName: () => "bootstrap.js", - }, - rollupOptions: { - external: [ - "electron", - "electron/main", - ...builtinModules, - ...builtinModules.map((m) => `node:${m}`), - "node-pty", - "@parcel/watcher", - "file-icon", - "better-sqlite3", - ], - onwarn(warning, warn) { - if (warning.code === "UNUSED_EXTERNAL_IMPORT") return; - if ( - warning.code === "EVAL" && - warning.id?.includes("web-tree-sitter") - ) - return; - warn(warning); - }, - }, - }, - }; -}); diff --git a/apps/code/vite.preload.config.mts b/apps/code/vite.preload.config.mts deleted file mode 100644 index f912952373..0000000000 --- a/apps/code/vite.preload.config.mts +++ /dev/null @@ -1,40 +0,0 @@ -import { builtinModules } from "node:module"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; -import { autoServicesPlugin } from "./vite-plugin-auto-services"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); - -export default defineConfig({ - plugins: [ - tsconfigPaths({ ignoreConfigErrors: true }), - autoServicesPlugin(path.join(__dirname, "src/main/services")), - ], - resolve: { - conditions: ["node"], - mainFields: ["module", "jsnext:main", "jsnext"], - }, - build: { - outDir: path.join(__dirname, ".vite/build"), - emptyOutDir: false, - rollupOptions: { - input: path.resolve(__dirname, "src/main/preload.ts"), - external: [ - "electron", - "electron/renderer", - "electron/common", - ...builtinModules, - ...builtinModules.map((m) => `node:${m}`), - ], - output: { - format: "cjs", - inlineDynamicImports: true, - entryFileNames: "preload.js", - chunkFileNames: "[name].js", - assetFileNames: "[name].[ext]", - }, - }, - }, -}); diff --git a/apps/code/vite.renderer.config.mts b/apps/code/vite.renderer.config.mts deleted file mode 100644 index 58d744e78c..0000000000 --- a/apps/code/vite.renderer.config.mts +++ /dev/null @@ -1,65 +0,0 @@ -import { readFileSync } from "node:fs"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import tailwindcss from "@tailwindcss/vite"; -import { devtools } from "@tanstack/devtools-vite"; -import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; -import react from "@vitejs/plugin-react"; -import { defineConfig, loadEnv } from "vite"; -import tsconfigPaths from "vite-tsconfig-paths"; -import { - createForceDevModeDefine, - createPosthogPlugin, - rendererAliases, -} from "./vite.shared.mjs"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const pkg = JSON.parse( - readFileSync(path.resolve(__dirname, "package.json"), "utf-8"), -); - -export default defineConfig(({ command, mode }) => { - const env = loadEnv(mode, path.resolve(__dirname, "../.."), ""); - - return { - base: command === "build" ? "./" : "/", - plugins: [ - // Source Inspector: hold Shift+Alt+Ctrl (⌘ on Mac) and click any element - // in dev to jump to its source. Dev-only so the data-tsd-source attrs it - // injects never ship in a packaged build. - mode === "development" && devtools(), - TanStackRouterVite({ - target: "react", - autoCodeSplitting: true, - routesDirectory: path.resolve( - __dirname, - "../../packages/ui/src/router/routes", - ), - generatedRouteTree: path.resolve( - __dirname, - "../../packages/ui/src/router/routeTree.gen.ts", - ), - }), - tailwindcss(), - react(), - tsconfigPaths({ ignoreConfigErrors: true }), - createPosthogPlugin(env, "posthog-code-renderer"), - ].filter(Boolean), - worker: { - format: "es", - }, - build: { - outDir: path.join(__dirname, ".vite/renderer/main_window"), - sourcemap: true, - }, - envDir: path.resolve(__dirname, "../.."), - define: { - ...createForceDevModeDefine(), - __APP_VERSION__: JSON.stringify(pkg.version), - }, - resolve: { - alias: rendererAliases, - dedupe: ["react", "react-dom"], - }, - }; -}); diff --git a/apps/code/vite.workspace-server.config.mts b/apps/code/vite.workspace-server.config.mts deleted file mode 100644 index 4687d710c7..0000000000 --- a/apps/code/vite.workspace-server.config.mts +++ /dev/null @@ -1,61 +0,0 @@ -import { builtinModules, createRequire } from "node:module"; -import path from "node:path"; -import { fileURLToPath } from "node:url"; -import { defineConfig } from "vite"; -import { mainAliases } from "./vite.shared.mjs"; - -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const require = createRequire(import.meta.url); - -const nodeBuiltins = new Set([ - ...builtinModules, - ...builtinModules.map((m) => `node:${m}`), -]); - -// Native modules (.node binaries) can't be bundled — they stay external and are -// resolved from the packaged node_modules at runtime, exactly as the main bundle -// treats them (see vite.main.config.mts). Everything else (pure JS) is bundled -// into workspace-server.js so the spawned child is self-contained and does not -// depend on node_modules being present next to the bundle in the packaged app. -const nativeModules = new Set([ - "@parcel/watcher", - "node-pty", - "better-sqlite3", - "file-icon", -]); - -const isExternal = (id: string): boolean => - nodeBuiltins.has(id) || nativeModules.has(id); - -export default defineConfig({ - resolve: { - alias: mainAliases, - conditions: ["node"], - }, - cacheDir: ".vite/cache-workspace-server", - // ssr.noExternal forces deps to be bundled; without it an SSR build leaves all - // node_modules imports external, which is what broke the packaged child. - ssr: { - noExternal: true, - external: [...nativeModules], - }, - build: { - target: "node18", - sourcemap: true, - minify: false, - reportCompressedSize: false, - outDir: path.join(__dirname, ".vite/build"), - emptyOutDir: false, - ssr: true, - lib: { - entry: require.resolve("@posthog/workspace-server/serve"), - formats: ["cjs"], - }, - rollupOptions: { - output: { - entryFileNames: "workspace-server.js", - }, - external: isExternal, - }, - }, -}); diff --git a/knip.json b/knip.json index 58d14ebc84..6dc2881c40 100644 --- a/knip.json +++ b/knip.json @@ -13,11 +13,9 @@ "src/renderer/main.tsx", "src/renderer/desktop-services.ts", "src/renderer/desktop-contributions.ts", - "vite.main.config.mts", - "vite.preload.config.mts", - "vite.renderer.config.mts", + "electron.vite.config.ts", + "vite-main-plugins.mts", "vite.shared.mts", - "vite.workspace-server.config.mts", "scripts/*.{ts,mjs}" ], "project": ["src/**/*.{ts,tsx}", "scripts/**/*.{ts,mjs}"], diff --git a/packages/agent/build/native-binary.mjs b/packages/agent/build/native-binary.mjs index b60e06afdc..dd78f863a7 100644 --- a/packages/agent/build/native-binary.mjs +++ b/packages/agent/build/native-binary.mjs @@ -5,7 +5,7 @@ import { join } from "node:path"; * via `@anthropic-ai/claude-agent-sdk-${platform}-${arch}` optional deps. * * Used by both `packages/agent/tsup.config.ts` (bundles the binary into the - * agent package's `dist/claude-cli/`) and `apps/code/vite.main.config.mts` + * agent package's `dist/claude-cli/`) and `apps/code/vite-main-plugins.mts` * (copies it into the Electron app's `.vite/build/claude-cli/`). * * The runtime equivalent of this lives upstream in `acp-agent.ts` as diff --git a/packages/agent/tsup.config.ts b/packages/agent/tsup.config.ts index 19269bb8d4..9dc82c7637 100644 --- a/packages/agent/tsup.config.ts +++ b/packages/agent/tsup.config.ts @@ -9,7 +9,7 @@ import { import { builtinModules } from "node:module"; import { dirname, resolve } from "node:path"; import { defineConfig } from "tsup"; -// Plain ESM helper, shared with apps/code/vite.main.config.mts. +// Plain ESM helper, shared with apps/code/vite-main-plugins.mts. import { CLAUDE_CLI_SUPPORT_DIRS, CLAUDE_CLI_SUPPORT_FILES, diff --git a/packages/workspace-server/src/services/agent/agent.ts b/packages/workspace-server/src/services/agent/agent.ts index 593981a00d..e0a663a59c 100644 --- a/packages/workspace-server/src/services/agent/agent.ts +++ b/packages/workspace-server/src/services/agent/agent.ts @@ -389,7 +389,7 @@ export class AgentService extends TypedEventEmitter { } private getClaudeCliPath(): string { - // Keep in sync with the destDir in apps/code/vite.main.config.mts + // Keep in sync with the destDir in apps/code/vite-main-plugins.mts // (copyClaudeExecutable plugin). const binary = process.platform === "win32" ? "claude.exe" : "claude"; return this.bundledResources.resolve(`.vite/build/claude-cli/${binary}`); diff --git a/packages/workspace-server/src/services/posthog-plugin/README.md b/packages/workspace-server/src/services/posthog-plugin/README.md index d3e2b91376..0ba679d3ca 100644 --- a/packages/workspace-server/src/services/posthog-plugin/README.md +++ b/packages/workspace-server/src/services/posthog-plugin/README.md @@ -24,7 +24,7 @@ A "skill name" is its directory name. If remote and shipped both have `query-dat ## Build Time -`copyPosthogPlugin()` in `vite.main.config.mts` assembles the plugin during `writeBundle`: +`copyPosthogPlugin()` in `vite-main-plugins.mts` assembles the plugin during `writeBundle`: 1. Copies allowed plugin entries into `.vite/build/plugins/posthog/` 2. Downloads `skills.zip` via `curl`, extracts with `unzip`, overlays into the build output diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4fb9699f4e..ea9f881427 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -382,6 +382,9 @@ importers: electron-builder: specifier: ^26.15.3 version: 26.15.3(electron-builder-squirrel-windows@26.15.3) + electron-vite: + specifier: ^4.0.1 + version: 4.0.1(vite@6.4.1(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)) husky: specifier: ^9.1.7 version: 9.1.7 @@ -7726,6 +7729,17 @@ packages: electron-updater@6.8.9: resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} + electron-vite@4.0.1: + resolution: {integrity: sha512-QqacJbA8f1pmwUTqki1qLL5vIBaOQmeq13CZZefZ3r3vKVaIoC7cpoTgE+KPKxJDFTax+iFZV0VYvLVWPiQ8Aw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@swc/core': ^1.0.0 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@swc/core': + optional: true + electron-winstaller@5.4.0: resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} engines: {node: '>=8.0.0'} @@ -19675,6 +19689,18 @@ snapshots: transitivePeerDependencies: - supports-color + electron-vite@4.0.1(vite@6.4.1(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)): + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0) + cac: 6.7.14 + esbuild: 0.25.12 + magic-string: 0.30.21 + picocolors: 1.1.1 + vite: 6.4.1(@types/node@24.12.0)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2) + transitivePeerDependencies: + - supports-color + electron-winstaller@5.4.0: dependencies: '@electron/asar': 3.4.1