Skip to content

Commit c65d091

Browse files
authored
Merge pull request #332 from cloudflare/nightly
Nightly -> Main
2 parents 6440eb5 + 22be3f8 commit c65d091

12 files changed

Lines changed: 634 additions & 10 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"knip:exports": "knip --exports"
4040
},
4141
"dependencies": {
42+
"acorn": "^8.14.0",
4243
"@ashishkumar472/cf-git": "1.0.5",
4344
"@babel/generator": "^7.28.5",
4445
"@babel/parser": "^7.28.5",
@@ -100,6 +101,7 @@
100101
"framer-motion": "^12.23.26",
101102
"hash-wasm": "^4.12.0",
102103
"hono": "^4.11.0",
104+
"htmlparser2": "^10.0.0",
103105
"html2canvas-pro": "^1.5.13",
104106
"input-otp": "^1.4.2",
105107
"inquirer": "^12.11.1",

worker/agents/core/behaviors/base.ts

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import type { DeepDebuggerInputs } from '../../operations/DeepDebugger';
4242
import { generatePortToken } from 'worker/utils/cryptoUtils';
4343
import { getPreviewDomain, getProtocolForHost } from 'worker/utils/urls';
4444
import { isDev } from 'worker/utils/envs';
45+
import { InMemoryAnalyzer } from '../../../services/static-analysis';
4546

4647
// Screenshot capture configuration
4748
const SCREENSHOT_CONFIG = {
@@ -589,13 +590,25 @@ export abstract class BaseCodingBehavior<TState extends BaseProjectState>
589590
*/
590591
async runStaticAnalysisCode(files?: string[]): Promise<StaticAnalysisResponse> {
591592
try {
592-
// Check if we have cached static analysis
593-
if (this.staticAnalysisCache) {
593+
// Only use cache for full (unscoped) analysis
594+
if (!files && this.staticAnalysisCache) {
594595
return this.staticAnalysisCache;
595596
}
596-
597-
const analysisResponse = await this.deploymentManager.runStaticAnalysis(files);
598-
this.staticAnalysisCache = analysisResponse;
597+
598+
// Use in-memory analysis for browser-rendered projects (no sandbox)
599+
const templateDetails = this.getTemplateDetails();
600+
let analysisResponse: StaticAnalysisResponse;
601+
602+
if (templateDetails?.renderMode === 'browser') {
603+
analysisResponse = await this.runInMemoryAnalysis(files);
604+
} else {
605+
analysisResponse = await this.deploymentManager.runStaticAnalysis(files);
606+
}
607+
608+
// Only cache full (unscoped) analysis results
609+
if (!files) {
610+
this.staticAnalysisCache = analysisResponse;
611+
}
599612

600613
const { lint, typecheck } = analysisResponse;
601614
this.broadcast(WebSocketMessageResponses.STATIC_ANALYSIS_RESULTS, {
@@ -610,6 +623,26 @@ export abstract class BaseCodingBehavior<TState extends BaseProjectState>
610623
}
611624
}
612625

626+
/**
627+
* Run in-memory static analysis for browser-rendered projects
628+
* Performs static analysis directly in the worker without using the sandbox
629+
*/
630+
private async runInMemoryAnalysis(filePaths?: string[]): Promise<StaticAnalysisResponse> {
631+
const allFiles = this.fileManager.getAllFiles();
632+
const filePathSet = filePaths ? new Set(filePaths) : null;
633+
const filesToAnalyze = filePathSet
634+
? allFiles.filter((f) => filePathSet.has(f.filePath))
635+
: allFiles;
636+
637+
const fileInputs = filesToAnalyze.map((f) => ({
638+
path: f.filePath,
639+
content: f.fileContents,
640+
}));
641+
642+
const analyzer = new InMemoryAnalyzer();
643+
return analyzer.analyze(fileInputs);
644+
}
645+
613646
/**
614647
* Apply deterministic code fixes for common TypeScript errors
615648
*/
@@ -685,8 +718,14 @@ export abstract class BaseCodingBehavior<TState extends BaseProjectState>
685718
}
686719

687720
async fetchAllIssues(resetIssues: boolean = false): Promise<AllIssues> {
688-
if (!this.state.sandboxInstanceId) {
689-
return { runtimeErrors: [], staticAnalysis: { success: false, lint: { issues: [], }, typecheck: { issues: [], } } };
721+
const templateDetails = this.getTemplateDetails();
722+
const isBrowserOnly = templateDetails?.renderMode === 'browser';
723+
724+
// For browser-rendered projects (no sandbox), only run static analysis
725+
if (isBrowserOnly) {
726+
const staticAnalysis = await this.runStaticAnalysisCode();
727+
this.logger.info("Fetched issues (browser-rendered):", JSON.stringify({ runtimeErrors: [], staticAnalysis }));
728+
return { runtimeErrors: [], staticAnalysis };
690729
}
691730
const [runtimeErrors, staticAnalysis] = await Promise.all([
692731
this.fetchRuntimeErrors(resetIssues),

worker/agents/prompts.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,9 +119,23 @@ and provide a preview url for the application.
119119
}
120120
},
121121

122-
serializeStaticAnalysis(staticAnalysis: StaticAnalysisResponse): string {
123-
const lintOutput = staticAnalysis.lint?.rawOutput || 'No linting issues detected';
124-
const typecheckOutput = staticAnalysis.typecheck?.rawOutput || 'No type checking issues detected';
122+
serializeStaticAnalysis(staticAnalysis: StaticAnalysisResponse, maxIssues = 20): string {
123+
const formatIssues = (issues: typeof staticAnalysis.lint.issues): string => {
124+
if (issues.length === 0) {
125+
return 'No issues detected';
126+
}
127+
const limitedIssues = issues.slice(0, maxIssues);
128+
const formatted = limitedIssues.map(issue =>
129+
`- [${issue.severity}] ${issue.filePath}:${issue.line}:${issue.column} - ${issue.message} (${issue.ruleId})`
130+
).join('\n');
131+
if (issues.length > maxIssues) {
132+
return `${formatted}\n... and ${issues.length - maxIssues} more issues (truncated)`;
133+
}
134+
return formatted;
135+
};
136+
137+
const lintOutput = staticAnalysis.lint.rawOutput || formatIssues(staticAnalysis.lint.issues);
138+
const typecheckOutput = staticAnalysis.typecheck.rawOutput || formatIssues(staticAnalysis.typecheck.issues);
125139

126140
return `**LINT ANALYSIS:**
127141
${lintOutput}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { IStaticAnalyzer, LanguageAnalyzer, CrossFileValidator, FileInput, StaticAnalysisResponse, CodeIssue } from './types';
2+
import { JavaScriptAnalyzer } from './analyzers/JavaScriptAnalyzer';
3+
import { HTMLAnalyzer } from './analyzers/HTMLAnalyzer';
4+
import { CSSAnalyzer } from './analyzers/CSSAnalyzer';
5+
import { HTMLCSSCrossValidator } from './validators/HTMLCSSCrossValidator';
6+
7+
export class InMemoryAnalyzer implements IStaticAnalyzer {
8+
private analyzers: LanguageAnalyzer[];
9+
private crossValidators: CrossFileValidator[];
10+
11+
constructor() {
12+
this.analyzers = [
13+
new JavaScriptAnalyzer(),
14+
new HTMLAnalyzer(),
15+
new CSSAnalyzer(),
16+
];
17+
this.crossValidators = [
18+
new HTMLCSSCrossValidator(),
19+
];
20+
}
21+
22+
async analyze(files: FileInput[]): Promise<StaticAnalysisResponse> {
23+
const allIssues: CodeIssue[] = [];
24+
25+
// Single-file analysis
26+
for (const file of files) {
27+
const analyzer = this.getAnalyzerForFile(file.path);
28+
if (analyzer) {
29+
const issues = analyzer.analyze(file);
30+
allIssues.push(...issues);
31+
}
32+
}
33+
34+
// Cross-file validation
35+
for (const validator of this.crossValidators) {
36+
const issues = validator.validate(files);
37+
allIssues.push(...issues);
38+
}
39+
40+
return this.buildResponse(allIssues);
41+
}
42+
43+
private getAnalyzerForFile(filePath: string): LanguageAnalyzer | null {
44+
const ext = this.getExtension(filePath);
45+
for (const analyzer of this.analyzers) {
46+
if (analyzer.supportedExtensions.includes(ext)) {
47+
return analyzer;
48+
}
49+
}
50+
return null;
51+
}
52+
53+
private getExtension(filePath: string): string {
54+
const lastDot = filePath.lastIndexOf('.');
55+
if (lastDot === -1) return '';
56+
return filePath.slice(lastDot).toLowerCase();
57+
}
58+
59+
private buildResponse(issues: CodeIssue[]): StaticAnalysisResponse {
60+
const errorCount = issues.filter((i) => i.severity === 'error').length;
61+
const warningCount = issues.filter((i) => i.severity === 'warning').length;
62+
const infoCount = issues.filter((i) => i.severity === 'info').length;
63+
64+
return {
65+
success: true,
66+
lint: {
67+
issues,
68+
summary: {
69+
errorCount,
70+
warningCount,
71+
infoCount,
72+
},
73+
},
74+
typecheck: {
75+
issues: [],
76+
summary: {
77+
errorCount: 0,
78+
warningCount: 0,
79+
infoCount: 0,
80+
},
81+
},
82+
};
83+
}
84+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import type { LanguageAnalyzer, FileInput, CodeIssue } from '../types';
2+
3+
export class CSSAnalyzer implements LanguageAnalyzer {
4+
readonly supportedExtensions = ['.css'];
5+
6+
analyze(file: FileInput): CodeIssue[] {
7+
const issues: CodeIssue[] = [];
8+
const content = file.content;
9+
const lines = content.split('\n');
10+
11+
let braceCount = 0;
12+
let inString = false;
13+
let stringChar = '';
14+
let inComment = false;
15+
16+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
17+
const line = lines[lineNum];
18+
19+
for (let i = 0; i < line.length; i++) {
20+
const char = line[i];
21+
const nextChar = line[i + 1];
22+
23+
// Handle comments
24+
if (!inString && !inComment && char === '/' && nextChar === '*') {
25+
inComment = true;
26+
i++;
27+
continue;
28+
}
29+
if (inComment && char === '*' && nextChar === '/') {
30+
inComment = false;
31+
i++;
32+
continue;
33+
}
34+
if (inComment) continue;
35+
36+
// Handle strings
37+
if (!inString && (char === '"' || char === "'")) {
38+
inString = true;
39+
stringChar = char;
40+
continue;
41+
}
42+
if (inString && char === stringChar && line[i - 1] !== '\\') {
43+
inString = false;
44+
continue;
45+
}
46+
if (inString) continue;
47+
48+
// Count braces
49+
if (char === '{') braceCount++;
50+
if (char === '}') braceCount--;
51+
52+
if (braceCount < 0) {
53+
issues.push({
54+
message: 'Unexpected closing brace',
55+
filePath: file.path,
56+
line: lineNum + 1,
57+
column: i,
58+
severity: 'error',
59+
ruleId: 'CSS_UNEXPECTED_BRACE',
60+
source: 'css-analyzer',
61+
});
62+
braceCount = 0;
63+
}
64+
}
65+
}
66+
67+
if (braceCount > 0) {
68+
issues.push({
69+
message: `Unclosed brace: ${braceCount} opening brace(s) without matching close`,
70+
filePath: file.path,
71+
line: lines.length,
72+
column: 0,
73+
severity: 'error',
74+
ruleId: 'CSS_UNCLOSED_BRACE',
75+
source: 'css-analyzer',
76+
});
77+
}
78+
79+
if (inComment) {
80+
issues.push({
81+
message: 'Unclosed comment',
82+
filePath: file.path,
83+
line: lines.length,
84+
column: 0,
85+
severity: 'error',
86+
ruleId: 'CSS_UNCLOSED_COMMENT',
87+
source: 'css-analyzer',
88+
});
89+
}
90+
91+
if (inString) {
92+
issues.push({
93+
message: 'Unclosed string',
94+
filePath: file.path,
95+
line: lines.length,
96+
column: 0,
97+
severity: 'error',
98+
ruleId: 'CSS_UNCLOSED_STRING',
99+
source: 'css-analyzer',
100+
});
101+
}
102+
103+
return issues;
104+
}
105+
}

0 commit comments

Comments
 (0)