Skip to content

Commit e07a04f

Browse files
committed
fix: restore unmodified files from cache during cleanup
1 parent ad24e03 commit e07a04f

File tree

7 files changed

+164
-5
lines changed

7 files changed

+164
-5
lines changed

apps/studio/electron/main/code/files.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, promises as fs } from 'fs';
22
import * as path from 'path';
33
import prettier from 'prettier';
4+
import crypto from 'crypto';
45

56
export async function readFile(filePath: string): Promise<string | null> {
67
try {
@@ -82,3 +83,27 @@ export async function formatContent(filePath: string, content: string): Promise<
8283
return content;
8384
}
8485
}
86+
87+
export function createHash(content: string): string {
88+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
89+
}
90+
91+
export async function checkIfCacheDirectoryExists(projectDir: string): Promise<void> {
92+
const cacheDir = path.join(projectDir, '.onlook', 'cache');
93+
try {
94+
await fs.mkdir(cacheDir, { recursive: true });
95+
} catch (error) {
96+
console.error(`Failed to create cache directory: ${error}`);
97+
}
98+
}
99+
100+
export async function removeCacheDirectory(projectDir: string): Promise<void> {
101+
const cacheDir = path.join(projectDir, '.onlook', 'cache');
102+
103+
try {
104+
await fs.rm(cacheDir, { recursive: true, force: true });
105+
console.log(`Removed cache directory: ${cacheDir}`);
106+
} catch (error) {
107+
console.error(`Failed to remove cache directory: ${error}`);
108+
}
109+
}

apps/studio/electron/main/run/cleanup.ts

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,19 @@ import traverse, { NodePath } from '@babel/traverse';
22
import * as t from '@babel/types';
33
import { EditorAttributes } from '@onlook/models/constants';
44
import { generateCode } from '../code/diff/helpers';
5-
import { formatContent, readFile, writeFile } from '../code/files';
5+
import { createHash, formatContent, readFile, writeFile } from '../code/files';
66
import { parseJsxFile } from '../code/helpers';
77
import { GENERATE_CODE_OPTIONS, getValidFiles, isReactFragment } from './helpers';
8+
import path from 'path';
9+
import type { HashesJson } from '@onlook/models';
810

911
export async function removeIdsFromDirectory(dirPath: string) {
1012
const filePaths = await getValidFiles(dirPath);
1113
for (const filePath of filePaths) {
12-
await removeIdsFromFile(filePath);
14+
const isFileChanged = await checkIfFileChanged(dirPath, filePath);
15+
if (isFileChanged) {
16+
await removeIdsFromFile(filePath);
17+
}
1318
}
1419
}
1520

@@ -67,3 +72,53 @@ export function removeIdsFromAst(ast: t.File) {
6772
},
6873
});
6974
}
75+
76+
export async function checkIfFileChanged(projectDir: string, filePath: string): Promise<boolean> {
77+
if (!filePath) {
78+
console.error('No file path provided.');
79+
return false;
80+
}
81+
82+
const cacheDir = path.join(projectDir, '.onlook', 'cache');
83+
const hashesFilePath = path.join(cacheDir, 'hashes.json');
84+
85+
let hashesJson: HashesJson = {};
86+
87+
try {
88+
const existing = await readFile(hashesFilePath);
89+
if (existing?.trim()) {
90+
hashesJson = JSON.parse(existing);
91+
}
92+
} catch (error) {
93+
console.error('Failed to read hashes.json. Proceeding without cache.');
94+
return false;
95+
}
96+
97+
const storedEntry = hashesJson[filePath];
98+
if (!storedEntry) {
99+
console.warn(`No stored hash for file: ${filePath}`);
100+
return true;
101+
}
102+
103+
const fileContentWithIds = await readFile(filePath);
104+
if (!fileContentWithIds || fileContentWithIds.trim() === '') {
105+
console.error(`Failed to get content for file: ${filePath}`);
106+
return false;
107+
}
108+
109+
const calculatedHash = createHash(fileContentWithIds);
110+
111+
if (calculatedHash === storedEntry.hash) {
112+
try {
113+
const cacheFileContent = await readFile(storedEntry.cache_path);
114+
if (cacheFileContent?.trim()) {
115+
await writeFile(filePath, cacheFileContent);
116+
return false;
117+
}
118+
} catch (err) {
119+
console.error(`Failed to read cached file at ${storedEntry.cache_path}:`, err);
120+
}
121+
}
122+
123+
return true;
124+
}

apps/studio/electron/main/run/helpers.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const IGNORED_DIRECTORIES = [
2222
'build',
2323
'.next',
2424
'.git',
25+
'.onlook',
2526
CUSTOM_OUTPUT_DIR,
2627
];
2728

apps/studio/electron/main/run/index.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,15 @@ import { RunState } from '@onlook/models/run';
44
import { subscribe, type AsyncSubscription } from '@parcel/watcher';
55
import { mainWindow } from '..';
66
import { sendAnalytics } from '../analytics';
7-
import { writeFile } from '../code/files';
7+
import { removeCacheDirectory, writeFile } from '../code/files';
88
import { removeIdsFromDirectory } from './cleanup';
99
import { ALLOWED_EXTENSIONS, getValidFiles, IGNORED_DIRECTORIES } from './helpers';
10-
import { createMappingFromContent, getFileWithIds as getFileContentWithIds } from './setup';
10+
import {
11+
cacheFile,
12+
createMappingFromContent,
13+
getFileWithIds as getFileContentWithIds,
14+
generateAndStoreHash,
15+
} from './setup';
1116
import terminal from './terminal';
1217

1318
class RunManager {
@@ -162,7 +167,9 @@ class RunManager {
162167
async addIdsToDirectoryAndCreateMapping(dirPath: string): Promise<string[]> {
163168
const filePaths = await getValidFiles(dirPath);
164169
for (const filePath of filePaths) {
170+
await cacheFile(filePath, dirPath);
165171
await this.processFileForMapping(filePath);
172+
await generateAndStoreHash(filePath, dirPath);
166173
}
167174
return filePaths;
168175
}
@@ -200,6 +207,7 @@ class RunManager {
200207

201208
async cleanProjectDir(folderPath: string): Promise<void> {
202209
await removeIdsFromDirectory(folderPath);
210+
await removeCacheDirectory(folderPath);
203211
this.runningDirs.delete(folderPath);
204212
}
205213

apps/studio/electron/main/run/setup.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
1+
import * as path from 'path';
2+
import { createHash } from 'crypto';
3+
import { promises as fs } from 'fs';
14
import traverse, { NodePath } from '@babel/traverse';
25
import * as t from '@babel/types';
36
import { EditorAttributes } from '@onlook/models/constants';
47
import type { DynamicType, TemplateNode } from '@onlook/models/element';
58
import { generateCode } from '../code/diff/helpers';
6-
import { formatContent, readFile } from '../code/files';
9+
import { checkIfCacheDirectoryExists, formatContent, readFile, writeFile } from '../code/files';
710
import { parseJsxFile } from '../code/helpers';
811
import {
912
GENERATE_CODE_OPTIONS,
@@ -14,6 +17,7 @@ import {
1417
isNodeElementArray,
1518
isReactFragment,
1619
} from './helpers';
20+
import type { HashesJson } from '@onlook/models';
1721

1822
export async function getFileWithIds(filePath: string): Promise<string | null> {
1923
const content = await readFile(filePath);
@@ -170,3 +174,60 @@ function createMapping(ast: t.File, filename: string): Record<string, TemplateNo
170174
});
171175
return mapping;
172176
}
177+
178+
export async function cacheFile(filePath: string, projectDir: string): Promise<void> {
179+
await checkIfCacheDirectoryExists(projectDir);
180+
181+
const content = await readFile(filePath);
182+
183+
if (!content || content.trim() === '') {
184+
console.error(`Failed to get content for file: ${filePath}`);
185+
return;
186+
}
187+
188+
const cacheDir = path.join(projectDir, '.onlook', 'cache');
189+
190+
const fileName = path.basename(filePath);
191+
192+
const cacheFilePath = path.join(cacheDir, fileName);
193+
194+
await writeFile(cacheFilePath, content);
195+
}
196+
197+
export async function generateAndStoreHash(filePath: string, projectDir: string) {
198+
await checkIfCacheDirectoryExists(projectDir);
199+
200+
const cacheDir = path.join(projectDir, '.onlook', 'cache');
201+
const hashesFilePath = path.join(cacheDir, 'hashes.json');
202+
203+
const content = await readFile(filePath);
204+
205+
if (!content || content.trim() === '') {
206+
console.error(`Failed to get content for file: ${filePath}`);
207+
return;
208+
}
209+
210+
const hash = createHash('sha256').update(content).digest('hex');
211+
212+
let hashesJson: HashesJson = {};
213+
214+
try {
215+
const existing = await readFile(hashesFilePath);
216+
if (existing) {
217+
hashesJson = JSON.parse(existing);
218+
}
219+
} catch (e) {
220+
console.log('No existing hashes.json found, creating new one.');
221+
}
222+
223+
const fileName = path.basename(filePath);
224+
225+
const cacheFilePath = path.join(cacheDir, fileName);
226+
227+
hashesJson[filePath] = {
228+
hash,
229+
cache_path: cacheFilePath,
230+
};
231+
232+
await fs.writeFile(hashesFilePath, JSON.stringify(hashesJson, null, 2), 'utf8');
233+
}

packages/models/src/cache/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
type HashEntry = {
2+
hash: string;
3+
cache_path: string;
4+
};
5+
6+
export type HashesJson = {
7+
[originalFilePath: string]: HashEntry;
8+
};

packages/models/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ export * from './pages/';
1313
export * from './projects/';
1414
export * from './run/';
1515
export * from './settings/';
16+
export * from './cache/';

0 commit comments

Comments
 (0)