From 31de4f06a0ef87cd6e76d7b1bbeca5281c08df3b Mon Sep 17 00:00:00 2001 From: Abu Bakkar Siddique Date: Sun, 20 Apr 2025 00:27:11 +0530 Subject: [PATCH 1/3] Added ESLint for linting the output of llm --- apps/studio/electron/main/create/index.ts | 42 +++++- packages/foundation/package.json | 7 +- packages/foundation/src/linting/index.ts | 167 ++++++++++++++++++++++ 3 files changed, 211 insertions(+), 5 deletions(-) create mode 100644 packages/foundation/src/linting/index.ts diff --git a/apps/studio/electron/main/create/index.ts b/apps/studio/electron/main/create/index.ts index 9485edb67c..21f9474384 100644 --- a/apps/studio/electron/main/create/index.ts +++ b/apps/studio/electron/main/create/index.ts @@ -16,6 +16,7 @@ import { mainWindow } from '..'; import Chat from '../chat'; import { getCreateProjectPath } from './helpers'; import { createProject } from './install'; +import { LintingService } from '@onlook/foundation/src/linting'; export class ProjectCreator { private static instance: ProjectCreator; @@ -65,7 +66,20 @@ export class ProjectCreator { throw new Error('AbortError'); } + // Apply the generated page with linting await this.applyGeneratedPage(projectPath, generatedPage); + + // Run a full project lint + const lintingService = LintingService.getInstance(); + const lintSummary = await lintingService.lintProject(projectPath); + + this.emitPromptProgress( + `Project linting completed: ${lintSummary.totalErrors} errors, ${ + lintSummary.totalWarnings + } warnings, ${lintSummary.fixedFiles} files auto-fixed`, + 95, + ); + return { projectPath, content: generatedPage.content }; }); } @@ -202,10 +216,30 @@ ${PAGE_SYSTEM_PROMPT.defaultContent}`; projectPath: string, generatedPage: { path: string; content: string }, ): Promise { - const pagePath = path.join(projectPath, generatedPage.path); - // Create recursive directories if they don't exist - await fs.promises.mkdir(path.dirname(pagePath), { recursive: true }); - await fs.promises.writeFile(pagePath, generatedPage.content); + const fullPath = path.join(projectPath, generatedPage.path); + await fs.promises.mkdir(path.dirname(fullPath), { recursive: true }); + + // Lint and fix the generated content + const lintingService = LintingService.getInstance(); + const lintResult = await lintingService.lintAndFix(fullPath, generatedPage.content); + + // Write the linted content + await fs.promises.writeFile(fullPath, lintResult.output || generatedPage.content); + + // Report linting results + if (lintResult.messages.length > 0) { + const errors = lintResult.messages.filter((m) => m.severity === 2); + const warnings = lintResult.messages.filter((m) => m.severity === 1); + + this.emitPromptProgress( + `Linting completed: ${errors.length} errors, ${warnings.length} warnings${ + lintResult.fixed ? ' (auto-fixed)' : '' + }`, + 90, + ); + } else { + this.emitPromptProgress('Linting completed: No issues found', 90); + } } private getStreamErrorMessage( diff --git a/packages/foundation/package.json b/packages/foundation/package.json index a7a1242225..d52291c9a7 100644 --- a/packages/foundation/package.json +++ b/packages/foundation/package.json @@ -44,6 +44,11 @@ "@babel/generator": "^7.14.5", "@babel/parser": "^7.14.3", "@babel/traverse": "^7.14.5", - "@babel/types": "^7.24.7" + "@babel/types": "^7.24.7", + "eslint": "^8.56.0", + "@typescript-eslint/parser": "^6.19.0", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0" } } diff --git a/packages/foundation/src/linting/index.ts b/packages/foundation/src/linting/index.ts new file mode 100644 index 0000000000..b5c38cbcc6 --- /dev/null +++ b/packages/foundation/src/linting/index.ts @@ -0,0 +1,167 @@ +import { ESLint } from 'eslint'; +import type { Linter } from 'eslint'; +import path from 'path'; +import fs from 'fs/promises'; + +type ESLintMessage = { + ruleId: string | null; + severity: number; + message: string; + line: number; + column: number; + nodeType?: string; + messageId?: string; + endLine?: number; + endColumn?: number; + fix?: { + range: [number, number]; + text: string; + }; +}; + +type ESLintResult = { + filePath: string; + messages: ESLintMessage[]; + fixed: boolean; + output?: string; +}; + +export interface LintResult { + filePath: string; + messages: ESLintMessage[]; + fixed: boolean; + output?: string; +} + +export interface LintSummary { + totalFiles: number; + totalErrors: number; + totalWarnings: number; + fixedFiles: number; + results: LintResult[]; +} + +export class LintingService { + private static instance: LintingService; + private eslint: ESLint; + + private constructor() { + this.eslint = new ESLint({ + fix: true, + baseConfig: { + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaFeatures: { + jsx: true, + }, + ecmaVersion: 12, + sourceType: 'module', + }, + plugins: ['@typescript-eslint', 'react', 'react-hooks'], + rules: { + 'react/react-in-jsx-scope': 'off', + 'react/prop-types': 'off', + '@typescript-eslint/no-unused-vars': ['warn'], + '@typescript-eslint/no-explicit-any': 'off', + 'no-console': 'warn', + }, + settings: { + react: { + version: 'detect', + }, + }, + } as unknown as Linter.Config, + }); + } + + public static getInstance(): LintingService { + if (!LintingService.instance) { + LintingService.instance = new LintingService(); + } + return LintingService.instance; + } + + public async lintAndFix(filePath: string, content: string): Promise { + const tempPath = path.join( + path.dirname(filePath), + `.temp.${Date.now()}.${path.basename(filePath)}`, + ); + try { + await fs.writeFile(tempPath, content); + + const results = await this.eslint.lintFiles([tempPath]); + if (!results.length) { + throw new Error('No lint result returned'); + } + + const result = results[0] as unknown as ESLintResult; + + let fixedContent = content; + if (result.output) { + fixedContent = result.output; + await fs.writeFile(tempPath, fixedContent); + } + + return { + filePath, + messages: result.messages, + fixed: result.fixed, + output: fixedContent, + }; + } catch (error) { + console.error('Linting error:', error); + return { + filePath, + messages: [], + fixed: false, + output: content, + }; + } finally { + await fs.unlink(tempPath).catch(() => {}); + } + } + + public async lintProject(projectPath: string): Promise { + const results: LintResult[] = []; + let totalErrors = 0; + let totalWarnings = 0; + let fixedFiles = 0; + + const files = await this.eslint.lintFiles([ + `${projectPath}/**/*.{ts,tsx,js,jsx}`, + `!${projectPath}/node_modules/**`, + ]); + const typedFiles = files as unknown as ESLintResult[]; + + for (const result of typedFiles) { + const messages = result.messages; + const hasErrors = messages.some((m) => m.severity === 2); + const hasWarnings = messages.some((m) => m.severity === 1); + + if (hasErrors) totalErrors += messages.filter((m) => m.severity === 2).length; + if (hasWarnings) totalWarnings += messages.filter((m) => m.severity === 1).length; + if (result.fixed) fixedFiles++; + + results.push({ + filePath: result.filePath, + messages, + fixed: result.fixed, + output: result.output, + }); + } + + return { + totalFiles: files.length, + totalErrors, + totalWarnings, + fixedFiles, + results, + }; + } +} From 8a54b418b4dbc71aae64fccbc5050ad4de3ae0a2 Mon Sep 17 00:00:00 2001 From: Abu Bakkar Siddique Date: Sun, 11 May 2025 17:25:47 +0530 Subject: [PATCH 2/3] Make it a toolcall --- apps/studio/electron/main/create/index.ts | 42 +------ packages/ai/src/tools/index.ts | 48 ++++++++ .../linting/index.ts => ai/src/tools/lint.ts} | 103 ++++++++++++------ 3 files changed, 121 insertions(+), 72 deletions(-) rename packages/{foundation/src/linting/index.ts => ai/src/tools/lint.ts} (60%) diff --git a/apps/studio/electron/main/create/index.ts b/apps/studio/electron/main/create/index.ts index 21f9474384..9485edb67c 100644 --- a/apps/studio/electron/main/create/index.ts +++ b/apps/studio/electron/main/create/index.ts @@ -16,7 +16,6 @@ import { mainWindow } from '..'; import Chat from '../chat'; import { getCreateProjectPath } from './helpers'; import { createProject } from './install'; -import { LintingService } from '@onlook/foundation/src/linting'; export class ProjectCreator { private static instance: ProjectCreator; @@ -66,20 +65,7 @@ export class ProjectCreator { throw new Error('AbortError'); } - // Apply the generated page with linting await this.applyGeneratedPage(projectPath, generatedPage); - - // Run a full project lint - const lintingService = LintingService.getInstance(); - const lintSummary = await lintingService.lintProject(projectPath); - - this.emitPromptProgress( - `Project linting completed: ${lintSummary.totalErrors} errors, ${ - lintSummary.totalWarnings - } warnings, ${lintSummary.fixedFiles} files auto-fixed`, - 95, - ); - return { projectPath, content: generatedPage.content }; }); } @@ -216,30 +202,10 @@ ${PAGE_SYSTEM_PROMPT.defaultContent}`; projectPath: string, generatedPage: { path: string; content: string }, ): Promise { - const fullPath = path.join(projectPath, generatedPage.path); - await fs.promises.mkdir(path.dirname(fullPath), { recursive: true }); - - // Lint and fix the generated content - const lintingService = LintingService.getInstance(); - const lintResult = await lintingService.lintAndFix(fullPath, generatedPage.content); - - // Write the linted content - await fs.promises.writeFile(fullPath, lintResult.output || generatedPage.content); - - // Report linting results - if (lintResult.messages.length > 0) { - const errors = lintResult.messages.filter((m) => m.severity === 2); - const warnings = lintResult.messages.filter((m) => m.severity === 1); - - this.emitPromptProgress( - `Linting completed: ${errors.length} errors, ${warnings.length} warnings${ - lintResult.fixed ? ' (auto-fixed)' : '' - }`, - 90, - ); - } else { - this.emitPromptProgress('Linting completed: No issues found', 90); - } + const pagePath = path.join(projectPath, generatedPage.path); + // Create recursive directories if they don't exist + await fs.promises.mkdir(path.dirname(pagePath), { recursive: true }); + await fs.promises.writeFile(pagePath, generatedPage.content); } private getStreamErrorMessage( diff --git a/packages/ai/src/tools/index.ts b/packages/ai/src/tools/index.ts index d16e1ead8e..d8e96b8266 100644 --- a/packages/ai/src/tools/index.ts +++ b/packages/ai/src/tools/index.ts @@ -4,6 +4,7 @@ import { readFile } from 'fs/promises'; import { z } from 'zod'; import { ONLOOK_PROMPT } from '../prompt/onlook'; import { getAllFiles } from './helpers'; +import { LintingService } from './lint'; export const listFilesTool = tool({ description: 'List all files in the current directory, including subdirectories', @@ -51,6 +52,52 @@ export const onlookInstructionsTool = tool({ }, }); +export const lintTool = tool({ + description: 'Analyze code quality and optionally fix issues in TypeScript/JavaScript files', + parameters: z.object({ + path: z.string().describe('The absolute path to the file or directory to lint'), + fix: z.boolean().optional().default(false).describe('Whether to auto-fix the issues'), + detailed: z + .boolean() + .optional() + .default(false) + .describe('Whether to return detailed rule-based statistics'), + }), + execute: async ({ path, fix = false, detailed = false }) => { + try { + const lintingService = LintingService.getInstance(fix); + const result = await lintingService.lintProject(path); + + if (!detailed) { + return { + summary: { + totalFiles: result.totalFiles, + totalErrors: result.totalErrors, + totalWarnings: result.totalWarnings, + fixedFiles: fix ? result.fixedFiles : undefined, + }, + fileResults: result.results.map((r) => ({ + file: r.filePath, + errors: r.messages.filter((m) => m.severity === 2).length, + warnings: r.messages.filter((m) => m.severity === 1).length, + ...(fix && { fixed: r.fixed }), + })), + }; + } + + return result; + } catch (error) { + return { + error: + error instanceof Error + ? error.message + : 'Unknown error occurred during linting', + success: false, + }; + } + }, +}); + // https://docs.anthropic.com/en/docs/agents-and-tools/computer-use#understand-anthropic-defined-tools // https://sdk.vercel.ai/docs/guides/computer-use#get-started-with-computer-use @@ -134,4 +181,5 @@ export const chatToolSet: ToolSet = { list_files: listFilesTool, read_files: readFilesTool, onlook_instructions: onlookInstructionsTool, + lint: lintTool, }; diff --git a/packages/foundation/src/linting/index.ts b/packages/ai/src/tools/lint.ts similarity index 60% rename from packages/foundation/src/linting/index.ts rename to packages/ai/src/tools/lint.ts index b5c38cbcc6..a49de8e262 100644 --- a/packages/foundation/src/linting/index.ts +++ b/packages/ai/src/tools/lint.ts @@ -5,7 +5,7 @@ import fs from 'fs/promises'; type ESLintMessage = { ruleId: string | null; - severity: number; + severity: 2 | 1 | 0; message: string; line: number; column: number; @@ -17,6 +17,13 @@ type ESLintMessage = { range: [number, number]; text: string; }; + suggestions?: { + desc: string; + fix: { + range: [number, number]; + text: string; + }; + }[]; }; type ESLintResult = { @@ -39,15 +46,21 @@ export interface LintSummary { totalWarnings: number; fixedFiles: number; results: LintResult[]; + errorsByRule: { + [ruleId: string]: number; + }; + warningsByRule: { + [ruleId: string]: number; + }; } export class LintingService { private static instance: LintingService; private eslint: ESLint; - private constructor() { + private constructor(fix: boolean = true) { this.eslint = new ESLint({ - fix: true, + fix, baseConfig: { extends: [ 'eslint:recommended', @@ -80,9 +93,9 @@ export class LintingService { }); } - public static getInstance(): LintingService { + public static getInstance(fix: boolean = true): LintingService { if (!LintingService.instance) { - LintingService.instance = new LintingService(); + LintingService.instance = new LintingService(fix); } return LintingService.instance; } @@ -115,7 +128,7 @@ export class LintingService { output: fixedContent, }; } catch (error) { - console.error('Linting error:', error); + console.error(`Failed to lint file: ${filePath}\n`, error); return { filePath, messages: [], @@ -132,36 +145,58 @@ export class LintingService { let totalErrors = 0; let totalWarnings = 0; let fixedFiles = 0; + const errorsByRule: { [key: string]: number } = {}; + const warningsByRule: { [key: string]: number } = {}; - const files = await this.eslint.lintFiles([ - `${projectPath}/**/*.{ts,tsx,js,jsx}`, - `!${projectPath}/node_modules/**`, - ]); - const typedFiles = files as unknown as ESLintResult[]; - - for (const result of typedFiles) { - const messages = result.messages; - const hasErrors = messages.some((m) => m.severity === 2); - const hasWarnings = messages.some((m) => m.severity === 1); - - if (hasErrors) totalErrors += messages.filter((m) => m.severity === 2).length; - if (hasWarnings) totalWarnings += messages.filter((m) => m.severity === 1).length; - if (result.fixed) fixedFiles++; + try { + const files = await this.eslint.lintFiles([ + `${projectPath}/**/*.{ts,tsx,js,jsx}`, + `!${projectPath}/node_modules/**`, + `!${projectPath}/**/dist/**`, + `!${projectPath}/**/build/**`, + `!${projectPath}/**/.next/**`, + ]); + + for (const result of files as unknown as ESLintResult[]) { + const messages = result.messages; + + // Process each message + messages.forEach((msg) => { + if (!msg.ruleId) return; + + if (msg.severity === 2) { + totalErrors++; + errorsByRule[msg.ruleId] = (errorsByRule[msg.ruleId] || 0) + 1; + } else if (msg.severity === 1) { + totalWarnings++; + warningsByRule[msg.ruleId] = (warningsByRule[msg.ruleId] || 0) + 1; + } + }); + + if (result.fixed) fixedFiles++; + + results.push({ + filePath: result.filePath, + messages, + fixed: result.fixed, + output: result.output, + }); + } - results.push({ - filePath: result.filePath, - messages, - fixed: result.fixed, - output: result.output, - }); + return { + totalFiles: files.length, + totalErrors, + totalWarnings, + fixedFiles, + results, + errorsByRule, + warningsByRule, + }; + } catch (error) { + console.error('Error during project linting:', error); + throw new Error( + `Failed to lint project: ${error instanceof Error ? error.message : 'Unknown error'}`, + ); } - - return { - totalFiles: files.length, - totalErrors, - totalWarnings, - fixedFiles, - results, - }; } } From 5ef71719591fc3123f482c75aeedb7f801f3428b Mon Sep 17 00:00:00 2001 From: Abu Bakkar Siddique Date: Sun, 11 May 2025 17:29:18 +0530 Subject: [PATCH 3/3] Modified package.json --- packages/ai/package.json | 7 ++++++- packages/foundation/package.json | 7 +------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/ai/package.json b/packages/ai/package.json index afd1a25cbe..ebe4d67283 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -35,6 +35,11 @@ "ai": "^4.2.6", "diff-match-patch": "^1.0.5", "fg": "^0.0.3", - "marked": "^15.0.7" + "marked": "^15.0.7", + "eslint": "^8.56.0", + "@typescript-eslint/parser": "^6.19.0", + "@typescript-eslint/eslint-plugin": "^6.19.0", + "eslint-plugin-react": "^7.33.2", + "eslint-plugin-react-hooks": "^4.6.0" } } diff --git a/packages/foundation/package.json b/packages/foundation/package.json index d52291c9a7..a7a1242225 100644 --- a/packages/foundation/package.json +++ b/packages/foundation/package.json @@ -44,11 +44,6 @@ "@babel/generator": "^7.14.5", "@babel/parser": "^7.14.3", "@babel/traverse": "^7.14.5", - "@babel/types": "^7.24.7", - "eslint": "^8.56.0", - "@typescript-eslint/parser": "^6.19.0", - "@typescript-eslint/eslint-plugin": "^6.19.0", - "eslint-plugin-react": "^7.33.2", - "eslint-plugin-react-hooks": "^4.6.0" + "@babel/types": "^7.24.7" } }