Skip to content

Commit e8f9efd

Browse files
committed
Harden dependency recovery and SSR imports
1 parent 3b8be7e commit e8f9efd

11 files changed

Lines changed: 276 additions & 189 deletions

File tree

app/components/@settings/tabs/github/components/GitHubConnection.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import { useState, type FormEvent } from 'react';
22
import { motion } from 'framer-motion';
33
import { Button } from '~/components/ui/Button';
44
import { cn } from '~/utils/cn';
@@ -21,10 +21,10 @@ interface GitHubConnectionProps {
2121
export function GitHubConnection({ connectionTest, onTestConnection }: GitHubConnectionProps) {
2222
const { isConnected, isLoading, isConnecting, connect, disconnect, error } = useGitHubConnection();
2323

24-
const [token, setToken] = React.useState('');
25-
const [tokenType, setTokenType] = React.useState<'classic' | 'fine-grained'>('classic');
24+
const [token, setToken] = useState('');
25+
const [tokenType, setTokenType] = useState<'classic' | 'fine-grained'>('classic');
2626

27-
const handleConnect = async (e: React.FormEvent) => {
27+
const handleConnect = async (e: FormEvent) => {
2828
e.preventDefault();
2929
logger.debug('handleConnect called with token:', token ? 'token provided' : 'no token', 'tokenType:', tokenType);
3030

app/components/@settings/tabs/providers/local/StatusDashboard.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React from 'react';
1+
import { createElement } from 'react';
22
import { Button } from '~/components/ui/Button';
33
import { Card, CardContent } from '~/components/ui/Card';
44
import { useLocalModelHealth } from '~/lib/hooks/useLocalModelHealth';
@@ -47,7 +47,7 @@ function StatusDashboard({ onBack }: { onBack: () => void }) {
4747
<div className="flex items-center gap-3">
4848
<div className="w-10 h-10 rounded-lg bg-devonz-elements-background-depth-3 flex items-center justify-center">
4949
{PROVIDER_ICONS[status.provider as keyof typeof PROVIDER_ICONS] ? (
50-
React.createElement(PROVIDER_ICONS[status.provider as keyof typeof PROVIDER_ICONS], {
50+
createElement(PROVIDER_ICONS[status.provider as keyof typeof PROVIDER_ICONS], {
5151
className: 'w-5 h-5 text-devonz-elements-textPrimary',
5252
})
5353
) : (

app/components/chat/FilePreview.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import React from 'react';
1+
import type { FC } from 'react';
22

33
interface FilePreviewProps {
44
files: File[];
55
imageDataList: string[];
66
onRemove: (index: number) => void;
77
}
88

9-
const FilePreview: React.FC<FilePreviewProps> = ({ files, imageDataList, onRemove }) => {
9+
const FilePreview: FC<FilePreviewProps> = ({ files, imageDataList, onRemove }) => {
1010
if (!files || files.length === 0) {
1111
return null;
1212
}

app/components/ui/SearchInput.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import React from 'react';
1+
import type { InputHTMLAttributes, Ref } from 'react';
22
import { cn } from '~/utils/cn';
33
import { Input } from './Input';
44
import { motion, AnimatePresence } from 'framer-motion';
55

6-
interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
6+
interface SearchInputProps extends InputHTMLAttributes<HTMLInputElement> {
77
/** Function to call when the clear button is clicked */
88
onClear?: () => void;
99

@@ -20,7 +20,7 @@ interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
2020
loading?: boolean;
2121

2222
/** Ref forwarded to the underlying input element */
23-
ref?: React.Ref<HTMLInputElement>;
23+
ref?: Ref<HTMLInputElement>;
2424
}
2525

2626
/**

app/lib/agent/prompts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ You operate in a local Node.js runtime on the user's machine.
9797
- NEVER use \`npm install <package>\` to add new dependencies — this does NOT update package.json
9898
- Instead, ALWAYS update package.json via devonz_write_file to add packages to dependencies/devDependencies
9999
- Then run a single \`npm install\` command to install everything
100+
- NEVER write \`"latest"\` in package.json — use the version already present in the template, a vetted compatible semver range, or skip the package if you're unsure
101+
- NEVER invent package names or use outdated/renamed packages; if a package name is uncertain, prefer an existing dependency or a built-in browser/React/Tailwind solution
102+
- When fixing a missing-package error, first verify whether the import should change before adding a new dependency
100103
- WRONG: \`npm install react-router-dom zustand\` (packages won't be in package.json)
101104
- RIGHT: Write updated package.json with new packages, then run \`npm install\`
102105

app/lib/common/prompts/new-prompt.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ export const getFineTunedPrompt = (
134134
- NEVER use "npm install <package>" shell commands to add new dependencies
135135
- Instead, ALWAYS update package.json via a devonzAction type="file" to add packages to "dependencies" or "devDependencies"
136136
- Then run a single "npm install" shell action to install everything at once
137+
- NEVER write \`"latest"\` in package.json — use the version already present in the template, a vetted compatible semver range, or skip the package if you're unsure
138+
- NEVER invent package names or use outdated/renamed packages; if a package name is uncertain, prefer an existing dependency or a built-in browser/React/Tailwind solution
139+
- When fixing a missing-package error, first verify whether the import should be changed before adding a new dependency
137140
- Why: Shell-only npm install does NOT persist dependencies in package.json, causing cascading failures when the dev server restarts
138141
- Correct workflow for adding new packages:
139142
1. Write updated package.json with new packages added to dependencies/devDependencies
@@ -144,7 +147,7 @@ export const getFineTunedPrompt = (
144147
</system_constraints>
145148
146149
<technology_preferences>
147-
- Use Vite for web servers (Vite 6 for stability, latest version with native Rolldown support for bleeding-edge)
150+
- Use Vite for web servers, but keep the version already present in package.json/template unless the user explicitly asks for an upgrade
148151
- NEVER hardcode port 5173 — it is reserved by the Devonz host runtime. If you need to set a port, use 3000
149152
- Do NOT set custom ports in vite.config or next.config unless the user explicitly requests a specific port
150153
- ALWAYS choose Node.js scripts over shell scripts

app/lib/runtime/action-runner.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
type ChangeType,
3232
} from '~/lib/stores/staging';
3333
import { workbenchStore } from '~/lib/stores/workbench';
34+
import { getPreferredPackageVersion } from '~/utils/dependencyCatalog';
3435

3536
const logger = createScopedLogger('ActionRunner');
3637

@@ -1130,13 +1131,37 @@ export class ActionRunner {
11301131
// Step 3: If missing packages found, inject into package.json and install
11311132
if (missingPackages.size > 0) {
11321133
const missing = [...missingPackages];
1133-
logger.info(`Dependency validator found ${missing.length} missing package(s): ${missing.join(', ')}`);
1134+
const installable: Array<{ name: string; version: string }> = [];
1135+
const unverified: string[] = [];
1136+
1137+
for (const pkg of missing) {
1138+
const preferredVersion = getPreferredPackageVersion(pkg);
1139+
1140+
if (preferredVersion) {
1141+
installable.push({ name: pkg, version: preferredVersion });
1142+
} else {
1143+
unverified.push(pkg);
1144+
}
1145+
}
1146+
1147+
logger.info(
1148+
`Dependency validator found ${missing.length} missing package(s): ${missing.join(', ')}. Auto-installing ${installable.length} vetted package(s).`,
1149+
);
1150+
1151+
if (unverified.length > 0) {
1152+
logger.warn(
1153+
`Skipped auto-install for ${unverified.length} unverified package(s): ${unverified.join(', ')}. Leaving the import error visible so Devonz can correct the package name instead of installing a hallucinated dependency.`,
1154+
);
1155+
}
1156+
1157+
if (installable.length === 0) {
1158+
return;
1159+
}
11341160

1135-
// Inject missing packages into package.json dependencies
11361161
const deps = (pkgJson.dependencies as Record<string, string>) || {};
11371162

1138-
for (const pkg of missing) {
1139-
deps[pkg] = 'latest';
1163+
for (const { name, version } of installable) {
1164+
deps[name] = version;
11401165
}
11411166

11421167
pkgJson.dependencies = deps;

app/lib/services/autoFixService.ts

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from '~/lib/stores/autofix';
2222
import { workbenchStore } from '~/lib/stores/workbench';
2323
import { getRecoverySuggestion } from '~/utils/errors/errorConfig';
24+
import { buildMissingPackageFixInstructions } from '~/utils/dependencyCatalog';
2425

2526
const logger = createScopedLogger('AutoFixService');
2627

@@ -83,17 +84,7 @@ function classifyError(error: AutoFixError): ClassifiedError {
8384
category: 'import-resolution',
8485
missingPackage: packageName,
8586
sourceFile,
86-
fixInstructions: [
87-
`**Root Cause**: The npm package \`${packageName}\` is imported in \`${sourceFile}\` but is NOT installed.`,
88-
'',
89-
'**Required Fix** (do ALL three steps):',
90-
`1. Add \`"${packageName}": "latest"\` to the \`"dependencies"\` object in \`package.json\` (use a file action, NOT npm install <pkg>)`,
91-
'2. Run `npm install --legacy-peer-deps` as a shell action',
92-
'3. Run `npm run dev` as a start action to restart the dev server',
93-
'',
94-
'**CRITICAL**: Do NOT modify the import statement in the source file — the import is correct, the package just needs to be installed.',
95-
'**CRITICAL**: Do NOT rewrite package.json from scratch — only ADD the missing package to the existing dependencies.',
96-
].join('\n'),
87+
fixInstructions: buildMissingPackageFixInstructions(packageName, sourceFile),
9788
};
9889
}
9990

@@ -155,14 +146,7 @@ function classifyError(error: AutoFixError): ClassifiedError {
155146
return {
156147
category: 'import-resolution',
157148
missingPackage: packageName,
158-
fixInstructions: [
159-
`**Root Cause**: Module \`${packageName}\` is not installed.`,
160-
'',
161-
'**Required Fix**:',
162-
`1. Add \`"${packageName}": "latest"\` to package.json dependencies`,
163-
'2. Run `npm install --legacy-peer-deps`',
164-
'3. Restart the dev server with `npm run dev`',
165-
].join('\n'),
149+
fixInstructions: buildMissingPackageFixInstructions(packageName),
166150
};
167151
}
168152
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/// <reference types="vitest/globals" />
2+
import { buildMissingPackageFixInstructions, getPreferredPackageVersion } from './dependencyCatalog';
3+
4+
describe('dependencyCatalog', () => {
5+
it('returns a curated version for vetted packages', () => {
6+
expect(getPreferredPackageVersion('framer-motion')).toBe('^11.15.0');
7+
expect(getPreferredPackageVersion('@radix-ui/react-dialog')).toBe('^1.1.2');
8+
});
9+
10+
it('returns undefined for unverified packages', () => {
11+
expect(getPreferredPackageVersion('totally-made-up-package')).toBeUndefined();
12+
});
13+
14+
it('builds known-package instructions without latest', () => {
15+
const instructions = buildMissingPackageFixInstructions('framer-motion', 'src/App.tsx');
16+
17+
expect(instructions).toContain('"framer-motion": "^11.15.0"');
18+
expect(instructions).toContain('src/App.tsx');
19+
expect(instructions).not.toContain('"latest"');
20+
});
21+
22+
it('requires verification for unknown packages', () => {
23+
const instructions = buildMissingPackageFixInstructions('totally-made-up-package');
24+
25+
expect(instructions).toContain('Verify that `totally-made-up-package` is a real, current npm package');
26+
expect(instructions).toContain('NEVER `"latest"`');
27+
});
28+
});

0 commit comments

Comments
 (0)