diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index b6de449a..ae5f985f 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -269,6 +269,144 @@ export async function precomputeDirListing( return (result.data as { entries?: DirEntry[] })?.entries ?? []; } +/** + * Common config file names that are frequently requested by multiple workflow + * steps (discover-context, detect-platform, plan-codemods). Pre-reading them + * eliminates 1-3 suspend/resume round-trips. + */ +const COMMON_CONFIG_FILES = [ + // ── Manifests (all ecosystems) ── + "package.json", + "tsconfig.json", + "pyproject.toml", + "requirements.txt", + "requirements-dev.txt", + "setup.py", + "setup.cfg", + "Pipfile", + "Gemfile", + "Gemfile.lock", + "go.mod", + "build.gradle", + "build.gradle.kts", + "settings.gradle", + "settings.gradle.kts", + "pom.xml", + "Cargo.toml", + "pubspec.yaml", + "mix.exs", + "composer.json", + "Podfile", + "CMakeLists.txt", + + // ── JavaScript/TypeScript framework configs ── + "next.config.js", + "next.config.mjs", + "next.config.ts", + "nuxt.config.ts", + "nuxt.config.js", + "angular.json", + "astro.config.mjs", + "astro.config.ts", + "svelte.config.js", + "remix.config.js", + "vite.config.ts", + "vite.config.js", + "webpack.config.js", + "metro.config.js", + "app.json", + "electron-builder.yml", + "wrangler.toml", + "wrangler.jsonc", + "serverless.yml", + "serverless.ts", + "bunfig.toml", + + // ── Python entry points / framework markers ── + "manage.py", + "app.py", + "main.py", + + // ── PHP framework markers ── + "artisan", + "symfony.lock", + "wp-config.php", + "config/packages/sentry.yaml", + + // ── .NET ── + "appsettings.json", + "Program.cs", + "Startup.cs", + + // ── Java / Android ── + "app/build.gradle", + "app/build.gradle.kts", + "src/main/resources/application.properties", + "src/main/resources/application.yml", + + // ── Ruby (Rails) ── + "config/application.rb", + + // ── Go entry point ── + "main.go", + + // ── Sentry configs (all ecosystems) ── + "sentry.client.config.ts", + "sentry.client.config.js", + "sentry.server.config.ts", + "sentry.server.config.js", + "sentry.edge.config.ts", + "sentry.edge.config.js", + "sentry.properties", + "instrumentation.ts", + "instrumentation.js", +]; + +const MAX_PREREAD_TOTAL_BYTES = 512 * 1024; + +/** + * Pre-read common config files that exist in the directory listing. + * Returns a fileCache map (path -> content or null) that the server + * can use to skip read-files suspend/resume round-trips. + */ +export async function preReadCommonFiles( + directory: string, + dirListing: DirEntry[] +): Promise> { + const listingPaths = new Set( + dirListing.map((e) => e.path.replaceAll("\\", "/")) + ); + const toRead = COMMON_CONFIG_FILES.filter((f) => listingPaths.has(f)); + + const cache: Record = {}; + let totalBytes = 0; + + for (const filePath of toRead) { + if (totalBytes >= MAX_PREREAD_TOTAL_BYTES) { + break; + } + try { + const absPath = path.join(directory, filePath); + let content = await fs.promises.readFile(absPath, "utf-8"); + if (filePath.endsWith(".json")) { + try { + content = JSON.stringify(JSON.parse(content)); + } catch { + // Not valid JSON — send as-is + } + } + if (totalBytes + content.length <= MAX_PREREAD_TOTAL_BYTES) { + cache[filePath] = content; + totalBytes += content.length; + } + } catch { + cache[filePath] = null; + } + } + + return cache; +} + export async function handleLocalOp( payload: LocalOpPayload, options: WizardOptions diff --git a/src/lib/init/wizard-runner.ts b/src/lib/init/wizard-runner.ts index 0c80b9e6..604ede63 100644 --- a/src/lib/init/wizard-runner.ts +++ b/src/lib/init/wizard-runner.ts @@ -47,6 +47,7 @@ import { detectExistingProject, handleLocalOp, precomputeDirListing, + preReadCommonFiles, resolveOrgSlug, tryGetExistingProject, } from "./local-ops.js"; @@ -636,12 +637,20 @@ export async function runWizard(initialOptions: WizardOptions): Promise { let result: WorkflowRunResult; try { const dirListing = await precomputeDirListing(directory); + const fileCache = await preReadCommonFiles(directory, dirListing); spin.message("Connecting to wizard..."); run = await workflow.createRun(); result = assertWorkflowResult( await withTimeout( run.startAsync({ - inputData: { directory, yes, dryRun, features, dirListing }, + inputData: { + directory, + yes, + dryRun, + features, + dirListing, + fileCache, + }, tracingOptions, }), API_TIMEOUT_MS, diff --git a/test/lib/init/local-ops.test.ts b/test/lib/init/local-ops.test.ts index fa936625..e3b29893 100644 --- a/test/lib/init/local-ops.test.ts +++ b/test/lib/init/local-ops.test.ts @@ -1273,9 +1273,6 @@ describe("grep", () => { beforeEach(() => { testDir = mkdtempSync(join("/tmp", "grep-test-")); options = makeOptions({ directory: testDir }); - // Init a git repo so git grep / git ls-files tier is exercised - const { execSync } = require("node:child_process"); - execSync("git init -q", { cwd: testDir }); writeFileSync( join(testDir, "app.ts"), 'import * as Sentry from "@sentry/node";\nSentry.init({ dsn: "..." });\n' @@ -1423,8 +1420,6 @@ describe("glob", () => { beforeEach(() => { testDir = mkdtempSync(join("/tmp", "glob-test-")); options = makeOptions({ directory: testDir }); - const { execSync } = require("node:child_process"); - execSync("git init -q", { cwd: testDir }); writeFileSync(join(testDir, "app.ts"), "x"); writeFileSync(join(testDir, "utils.ts"), "x"); writeFileSync(join(testDir, "config.json"), "{}"); diff --git a/test/lib/init/wizard-runner.test.ts b/test/lib/init/wizard-runner.test.ts index b14f2ce1..56cea2a7 100644 --- a/test/lib/init/wizard-runner.test.ts +++ b/test/lib/init/wizard-runner.test.ts @@ -198,6 +198,7 @@ beforeEach(() => { ops, "precomputeDirListing" ).mockResolvedValue([]); + spyOn(ops, "preReadCommonFiles").mockResolvedValue({}); handleInteractiveSpy = spyOn(inter, "handleInteractive").mockResolvedValue({ action: "continue", }); @@ -400,9 +401,10 @@ describe("runWizard", { timeout: TEST_TIMEOUT_MS }, () => { const promise = runWizard(makeOptions()); // Flush microtasks so runWizard reaches the withTimeout setTimeout. - // preamble() → confirmExperimental() → checkGitStatus() → createRun() + // preamble() → confirmExperimental() → checkGitStatus() → + // precomputeDirListing() → preReadCommonFiles() → createRun() // each need a tick. - for (let i = 0; i < 10; i++) await Promise.resolve(); + for (let i = 0; i < 20; i++) await Promise.resolve(); // Advance past the timeout jest.advanceTimersByTime(API_TIMEOUT_MS);