Skip to content

Commit 31de4f0

Browse files
committed
Added ESLint for linting the output of llm
1 parent afee570 commit 31de4f0

File tree

3 files changed

+211
-5
lines changed

3 files changed

+211
-5
lines changed

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { mainWindow } from '..';
1616
import Chat from '../chat';
1717
import { getCreateProjectPath } from './helpers';
1818
import { createProject } from './install';
19+
import { LintingService } from '@onlook/foundation/src/linting';
1920

2021
export class ProjectCreator {
2122
private static instance: ProjectCreator;
@@ -65,7 +66,20 @@ export class ProjectCreator {
6566
throw new Error('AbortError');
6667
}
6768

69+
// Apply the generated page with linting
6870
await this.applyGeneratedPage(projectPath, generatedPage);
71+
72+
// Run a full project lint
73+
const lintingService = LintingService.getInstance();
74+
const lintSummary = await lintingService.lintProject(projectPath);
75+
76+
this.emitPromptProgress(
77+
`Project linting completed: ${lintSummary.totalErrors} errors, ${
78+
lintSummary.totalWarnings
79+
} warnings, ${lintSummary.fixedFiles} files auto-fixed`,
80+
95,
81+
);
82+
6983
return { projectPath, content: generatedPage.content };
7084
});
7185
}
@@ -202,10 +216,30 @@ ${PAGE_SYSTEM_PROMPT.defaultContent}`;
202216
projectPath: string,
203217
generatedPage: { path: string; content: string },
204218
): Promise<void> {
205-
const pagePath = path.join(projectPath, generatedPage.path);
206-
// Create recursive directories if they don't exist
207-
await fs.promises.mkdir(path.dirname(pagePath), { recursive: true });
208-
await fs.promises.writeFile(pagePath, generatedPage.content);
219+
const fullPath = path.join(projectPath, generatedPage.path);
220+
await fs.promises.mkdir(path.dirname(fullPath), { recursive: true });
221+
222+
// Lint and fix the generated content
223+
const lintingService = LintingService.getInstance();
224+
const lintResult = await lintingService.lintAndFix(fullPath, generatedPage.content);
225+
226+
// Write the linted content
227+
await fs.promises.writeFile(fullPath, lintResult.output || generatedPage.content);
228+
229+
// Report linting results
230+
if (lintResult.messages.length > 0) {
231+
const errors = lintResult.messages.filter((m) => m.severity === 2);
232+
const warnings = lintResult.messages.filter((m) => m.severity === 1);
233+
234+
this.emitPromptProgress(
235+
`Linting completed: ${errors.length} errors, ${warnings.length} warnings${
236+
lintResult.fixed ? ' (auto-fixed)' : ''
237+
}`,
238+
90,
239+
);
240+
} else {
241+
this.emitPromptProgress('Linting completed: No issues found', 90);
242+
}
209243
}
210244

211245
private getStreamErrorMessage(

packages/foundation/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
"@babel/generator": "^7.14.5",
4545
"@babel/parser": "^7.14.3",
4646
"@babel/traverse": "^7.14.5",
47-
"@babel/types": "^7.24.7"
47+
"@babel/types": "^7.24.7",
48+
"eslint": "^8.56.0",
49+
"@typescript-eslint/parser": "^6.19.0",
50+
"@typescript-eslint/eslint-plugin": "^6.19.0",
51+
"eslint-plugin-react": "^7.33.2",
52+
"eslint-plugin-react-hooks": "^4.6.0"
4853
}
4954
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { ESLint } from 'eslint';
2+
import type { Linter } from 'eslint';
3+
import path from 'path';
4+
import fs from 'fs/promises';
5+
6+
type ESLintMessage = {
7+
ruleId: string | null;
8+
severity: number;
9+
message: string;
10+
line: number;
11+
column: number;
12+
nodeType?: string;
13+
messageId?: string;
14+
endLine?: number;
15+
endColumn?: number;
16+
fix?: {
17+
range: [number, number];
18+
text: string;
19+
};
20+
};
21+
22+
type ESLintResult = {
23+
filePath: string;
24+
messages: ESLintMessage[];
25+
fixed: boolean;
26+
output?: string;
27+
};
28+
29+
export interface LintResult {
30+
filePath: string;
31+
messages: ESLintMessage[];
32+
fixed: boolean;
33+
output?: string;
34+
}
35+
36+
export interface LintSummary {
37+
totalFiles: number;
38+
totalErrors: number;
39+
totalWarnings: number;
40+
fixedFiles: number;
41+
results: LintResult[];
42+
}
43+
44+
export class LintingService {
45+
private static instance: LintingService;
46+
private eslint: ESLint;
47+
48+
private constructor() {
49+
this.eslint = new ESLint({
50+
fix: true,
51+
baseConfig: {
52+
extends: [
53+
'eslint:recommended',
54+
'plugin:@typescript-eslint/recommended',
55+
'plugin:react/recommended',
56+
'plugin:react-hooks/recommended',
57+
],
58+
parser: '@typescript-eslint/parser',
59+
parserOptions: {
60+
ecmaFeatures: {
61+
jsx: true,
62+
},
63+
ecmaVersion: 12,
64+
sourceType: 'module',
65+
},
66+
plugins: ['@typescript-eslint', 'react', 'react-hooks'],
67+
rules: {
68+
'react/react-in-jsx-scope': 'off',
69+
'react/prop-types': 'off',
70+
'@typescript-eslint/no-unused-vars': ['warn'],
71+
'@typescript-eslint/no-explicit-any': 'off',
72+
'no-console': 'warn',
73+
},
74+
settings: {
75+
react: {
76+
version: 'detect',
77+
},
78+
},
79+
} as unknown as Linter.Config,
80+
});
81+
}
82+
83+
public static getInstance(): LintingService {
84+
if (!LintingService.instance) {
85+
LintingService.instance = new LintingService();
86+
}
87+
return LintingService.instance;
88+
}
89+
90+
public async lintAndFix(filePath: string, content: string): Promise<LintResult> {
91+
const tempPath = path.join(
92+
path.dirname(filePath),
93+
`.temp.${Date.now()}.${path.basename(filePath)}`,
94+
);
95+
try {
96+
await fs.writeFile(tempPath, content);
97+
98+
const results = await this.eslint.lintFiles([tempPath]);
99+
if (!results.length) {
100+
throw new Error('No lint result returned');
101+
}
102+
103+
const result = results[0] as unknown as ESLintResult;
104+
105+
let fixedContent = content;
106+
if (result.output) {
107+
fixedContent = result.output;
108+
await fs.writeFile(tempPath, fixedContent);
109+
}
110+
111+
return {
112+
filePath,
113+
messages: result.messages,
114+
fixed: result.fixed,
115+
output: fixedContent,
116+
};
117+
} catch (error) {
118+
console.error('Linting error:', error);
119+
return {
120+
filePath,
121+
messages: [],
122+
fixed: false,
123+
output: content,
124+
};
125+
} finally {
126+
await fs.unlink(tempPath).catch(() => {});
127+
}
128+
}
129+
130+
public async lintProject(projectPath: string): Promise<LintSummary> {
131+
const results: LintResult[] = [];
132+
let totalErrors = 0;
133+
let totalWarnings = 0;
134+
let fixedFiles = 0;
135+
136+
const files = await this.eslint.lintFiles([
137+
`${projectPath}/**/*.{ts,tsx,js,jsx}`,
138+
`!${projectPath}/node_modules/**`,
139+
]);
140+
const typedFiles = files as unknown as ESLintResult[];
141+
142+
for (const result of typedFiles) {
143+
const messages = result.messages;
144+
const hasErrors = messages.some((m) => m.severity === 2);
145+
const hasWarnings = messages.some((m) => m.severity === 1);
146+
147+
if (hasErrors) totalErrors += messages.filter((m) => m.severity === 2).length;
148+
if (hasWarnings) totalWarnings += messages.filter((m) => m.severity === 1).length;
149+
if (result.fixed) fixedFiles++;
150+
151+
results.push({
152+
filePath: result.filePath,
153+
messages,
154+
fixed: result.fixed,
155+
output: result.output,
156+
});
157+
}
158+
159+
return {
160+
totalFiles: files.length,
161+
totalErrors,
162+
totalWarnings,
163+
fixedFiles,
164+
results,
165+
};
166+
}
167+
}

0 commit comments

Comments
 (0)