From b5a6a1eeab663c2d6aaec2c89eab7a550cb0bb6b Mon Sep 17 00:00:00 2001 From: Ruben Fiszel Date: Thu, 4 Jun 2026 09:30:56 +0200 Subject: [PATCH] fix(cli): push whole raw app instead of treating frontend files as scripts (#9442) * fix(cli): push whole raw app instead of treating frontend files as scripts Co-Authored-By: Claude Opus 4.8 (1M context) * docs(cli): shorten raw-app handleFile comment Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- cli/src/commands/script/script.ts | 4 +- cli/src/commands/sync/sync.ts | 9 ++-- cli/test/raw_app_sync.test.ts | 75 +++++++++++++++++++++++++++++++ 3 files changed, 83 insertions(+), 5 deletions(-) diff --git a/cli/src/commands/script/script.ts b/cli/src/commands/script/script.ts index 0299744056..717e8acf46 100644 --- a/cli/src/commands/script/script.ts +++ b/cli/src/commands/script/script.ts @@ -297,7 +297,9 @@ export async function handleFile( if ( !isAppInlineScriptPath(path) && !isFlowInlineScriptPath(path) && - !isRawAppBackendPath(path) && + // Raw-app files (frontend included) belong to the app bundle, never + // standalone scripts — pushed via pushRawApp, not here. + !isRawAppPath(path) && (!isScriptModulePath(path) || moduleEntryPoint) && exts.some((exts) => path.endsWith(exts)) ) { diff --git a/cli/src/commands/sync/sync.ts b/cli/src/commands/sync/sync.ts index 2070ceb590..ed3b7828f1 100644 --- a/cli/src/commands/sync/sync.ts +++ b/cli/src/commands/sync/sync.ts @@ -4066,10 +4066,11 @@ export async function push( continue; } if ( - change.path.endsWith(".script.json") || - change.path.endsWith(".script.yaml") || - change.path.endsWith(".lock") || - isFileResource(change.path) + !isRawAppFile(change.path) && + (change.path.endsWith(".script.json") || + change.path.endsWith(".script.yaml") || + change.path.endsWith(".lock") || + isFileResource(change.path)) ) { continue; } else if ( diff --git a/cli/test/raw_app_sync.test.ts b/cli/test/raw_app_sync.test.ts index ee20bc1947..3cf1172daa 100644 --- a/cli/test/raw_app_sync.test.ts +++ b/cli/test/raw_app_sync.test.ts @@ -382,6 +382,81 @@ excludes: []`, "utf-8"); }); }); +test("Raw App: frontend .ts file sorting first does not short-circuit the app push", async () => { + // Regression: in the push apply loop, raw-app changes are collapsed to a + // single representative change (changes[0]). Because every file inside a + // raw_app folder shares the same sort order, changes[0] is just the + // alphabetically-first changed path. When that path was a frontend file + // with a script extension (e.g. "Api.ts", which sorts before "App.tsx"), + // handleFile() mistook it for a standalone script: it pushed a bogus script + // at the truncated path (f/test/) AND returned true, so the loop + // `continue`d and pushRawApp() never ran. Result: the whole raw app silently + // failed to deploy while the CLI still reported success. + await withTestBackend(async (backend, tempDir) => { + const testWorkspace = { + remote: backend.baseUrl, + workspaceId: backend.workspace, + name: "raw_app_ts_first_test", + token: backend.token + }; + await addWorkspace(testWorkspace, { force: true, configDir: backend.testConfigDir }); + + await writeFile(`${tempDir}/wmill.yaml`, `defaultTs: bun +includes: + - "**" +excludes: []`, "utf-8"); + + const appDir = path.join(tempDir, "f", "test", "ts_first_app.raw_app"); + await mkdir(path.join(tempDir, "f", "test"), { recursive: true }); + await createRawAppOnDisk(appDir); + + // A frontend .ts file whose name sorts before "App.tsx". + const apiTsPath = path.join(appDir, "Api.ts"); + await writeFile(apiTsPath, "export const API = '/api/v1'\n", "utf-8"); + + // Initial push: create the raw app on the backend. + const pushResult1 = await backend.runCLICommand( + ['sync', 'push', '--yes'], + tempDir, "raw_app_ts_first_test" + ); + expect(pushResult1.code).toEqual(0); + + // Edit App.tsx (and the .ts file that sorts first) and push again. + const appTsxPath = path.join(appDir, "App.tsx"); + const appTsxContent = await readFileContent(appTsxPath); + await writeFile( + appTsxPath, + appTsxContent.replace("hello world", "REGRESSION MARKER"), + "utf-8" + ); + await writeFile(apiTsPath, "export const API = '/api/v2'\n", "utf-8"); + + const pushResult2 = await backend.runCLICommand( + ['sync', 'push', '--yes'], + tempDir, "raw_app_ts_first_test" + ); + expect(pushResult2.code).toEqual(0); + + // The App.tsx edit must have landed on the remote app's bundled files. + const appResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/apps/get/p/f/test/ts_first_app` + ); + expect(appResp.status).toEqual(200); + const appJson = await appResp.json(); + const files = appJson?.value?.files ?? {}; + expect(files["/App.tsx"]).toContain("REGRESSION MARKER"); + // The first-sorting .ts file is part of the app bundle, with fresh content. + expect(files["/Api.ts"]).toContain("/api/v2"); + + // And no bogus standalone script was created at the truncated path + // (f/test/ts_first_app.raw_app/Api.ts -> f/test/ts_first_app). + const scriptResp = await backend.apiRequest!( + `/api/w/${backend.workspace}/scripts/get/p/f/test/ts_first_app` + ); + expect(scriptResp.status).toEqual(404); + }); +}); + test("Raw App: delete file and push", async () => { await withTestBackend(async (backend, tempDir) => { // Set up workspace