From 7ad29702c03cd9f135b46a2aad43553637bb2971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miguel=20Beteg=C3=B3n?= Date: Thu, 9 Apr 2026 15:42:18 +0200 Subject: [PATCH] feat(init): pre-read common config files to reduce round-trips After computing the directory listing, pre-read ~35 common config files (manifests, framework configs, Sentry configs) if they exist. Send the results as fileCache in the startAsync input data. Workflow steps already check fileCache before suspending for read-files, so this eliminates 1-3 HTTP round-trips with no server-side changes needed (the fileCache field is already in step schemas). Capped at 512KB total to avoid sending excessive data upfront. Made-with: Cursor --- src/lib/init/local-ops.ts | 77 +++++++++++++++++++++++++++++++++++ src/lib/init/wizard-runner.ts | 11 ++++- 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/lib/init/local-ops.ts b/src/lib/init/local-ops.ts index 4908bac3e..89ce9c298 100644 --- a/src/lib/init/local-ops.ts +++ b/src/lib/init/local-ops.ts @@ -284,6 +284,83 @@ 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 = [ + "package.json", + "tsconfig.json", + "pyproject.toml", + "Gemfile", + "go.mod", + "build.gradle", + "build.gradle.kts", + "pom.xml", + "Cargo.toml", + "pubspec.yaml", + "mix.exs", + "composer.json", + "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", + "sentry.client.config.ts", + "sentry.client.config.js", + "sentry.server.config.ts", + "sentry.server.config.js", + "sentry.edge.config.ts", + "sentry.edge.config.js", + "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)); + 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); + const content = await fs.promises.readFile(absPath, "utf-8"); + 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 ed08a977b..b8a9b3731 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"; @@ -616,12 +617,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,