Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/icy-queens-greet.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sveltejs/mcp': patch
---

fix: add `async` parameter to `svelte-autofixer`
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ export function add_autofixers_issues(
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
async = false,
) {
const parsed = parse(code, filename);

// Run each autofixer separately to avoid interrupting logic flow
for (const autofixer of Object.values(autofixers)) {
walk(
parsed.ast as unknown as Node,
{ output: content, parsed, desired_svelte_version },
{ output: content, parsed, desired_svelte_version, async },
autofixer,
);
}
Expand Down
2 changes: 2 additions & 0 deletions packages/mcp-server/src/mcp/autofixers/add-compile-issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export function add_compile_issues(
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
async = false,
) {
let compile = compile_component;
const extension = extname(filename);
Expand All @@ -27,6 +28,7 @@ export function add_compile_issues(
filename: filename || 'Component.svelte',
generate: false,
runes: desired_svelte_version >= 5,
experimental: { async },
});

for (const warning of compilation_result.warnings) {
Expand Down
6 changes: 4 additions & 2 deletions packages/mcp-server/src/mcp/autofixers/add-eslint-issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
];
}

function get_linter(version: number) {
function get_linter(version: number, async = false) {
if (version < 5) {
return (svelte_4_linter ??= new ESLint({
overrideConfigFile: true,
Expand All @@ -67,6 +67,7 @@ function get_linter(version: number) {
baseConfig: base_config({
compilerOptions: {
runes: true,
experimental: { async },
},
}),
}));
Expand All @@ -77,8 +78,9 @@ export async function add_eslint_issues(
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
async = false,
) {
const eslint = get_linter(desired_svelte_version);
const eslint = get_linter(desired_svelte_version, async);
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });

for (const message of results[0]?.messages ?? []) {
Expand Down
1 change: 1 addition & 0 deletions packages/mcp-server/src/mcp/autofixers/visitors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export type AutofixerState = {
output: { issues: string[]; suggestions: string[] };
parsed: ParseResult;
desired_svelte_version: number;
async?: boolean;
};

export type Autofixer = Visitors<Node | AST.SvelteNode, AutofixerState>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ function request<const T>(request: T) {
return request;
}

async function autofixer_tool_call(code: string, is_error = false, desired_svelte_version = 5) {
async function autofixer_tool_call(
code: string,
is_error = false,
desired_svelte_version = 5,
async = false,
) {
const result = await server.receive({
jsonrpc: '2.0',
id: 2,
Expand All @@ -19,6 +24,7 @@ async function autofixer_tool_call(code: string, is_error = false, desired_svelt
code,
desired_svelte_version,
filename: 'App.svelte',
async,
},
},
});
Expand Down Expand Up @@ -65,6 +71,61 @@ describe('svelte-autofixer tool', () => {
);
});

it('should error out if async is true with a version less than 5', async () => {
const content = await autofixer_tool_call(
`<script>
$state count = 0;
</script>`,
true,
4,
true,
);
expect(content.isError).toBeTruthy();
expect(content.content[0]).toBeDefined();
expect(content.content[0].text).toBe(
'The async option can only be used with Svelte version 5 or higher.',
);
});

it('should not add suggestion/issues if async is true and await is used in the template/derived', async () => {
const content = await autofixer_tool_call(
`<script>
import { slow_double } from './utils.js';
let count = $state(0);
let double = $derived(await slow_double(count));
</script>

{double}
{await slow_double(count)}`,
false,
5,
true,
);
expect(content.issues).toHaveLength(0);
expect(content.suggestions).toHaveLength(0);
expect(content.require_another_tool_call_after_fixing).toBeFalsy();
});

it('should add suggestion/issues if async is false and await is used in the template/derived', async () => {
const content = await autofixer_tool_call(
`<script>
import { slow_double } from './utils.js';
let count = $state(0);
let double = $derived(await slow_double(count));
</script>

{double}
{await slow_double(count)}`,
false,
5,
);
expect(content.issues.length).toBeGreaterThanOrEqual(1);
expect(content.issues).toEqual(
expect.arrayContaining([expect.stringContaining('experimental_async')]),
);
expect(content.require_another_tool_call_after_fixing).toBeTruthy();
});

it('should add suggestions for css invalid identifier', async () => {
const content = await autofixer_tool_call(`<script>
let my_color = $state('red');
Expand Down
25 changes: 22 additions & 3 deletions packages/mcp-server/src/mcp/handlers/tools/svelte-autofixer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ export function svelte_autofixer(server: SvelteMcp) {
'The desired svelte version...if possible read this from the package.json of the user project, otherwise use some hint from the wording (if the user asks for runes it wants version 5). Default to 5 in case of doubt.',
),
),
async: v.pipe(
v.optional(v.boolean()),
v.description(
'If true the code is an async component/module and might use await in the markup or top-level awaits in the script tag. If possible check the svelte.config.js/svelte.config.ts to check if the option is enabled otherwise asks the user if they prefer using it or not. You can only use this option if the version is 5.',
),
),
filename: v.pipe(
v.optional(v.string()),
v.description(
Expand All @@ -45,6 +51,7 @@ export function svelte_autofixer(server: SvelteMcp) {
code,
filename: filename_or_path,
desired_svelte_version: desired_svelte_version_unchecked,
async,
}) => {
if (server.ctx.sessionId && server.ctx.custom?.track) {
await server.ctx.custom?.track?.(server.ctx.sessionId, 'svelte-autofixer');
Expand All @@ -71,6 +78,18 @@ export function svelte_autofixer(server: SvelteMcp) {

const desired_svelte_version = parsed_version.output;

if (async && +desired_svelte_version < 5) {
return {
isError: true,
content: [
{
type: 'text',
text: `The async option can only be used with Svelte version 5 or higher.`,
},
],
};
}

const content: {
issues: string[];
suggestions: string[];
Expand All @@ -82,11 +101,11 @@ export function svelte_autofixer(server: SvelteMcp) {

const filename = filename_or_path ? basename(filename_or_path) : 'Component.svelte';

add_compile_issues(content, code, +desired_svelte_version, filename);
add_compile_issues(content, code, +desired_svelte_version, filename, async);

add_autofixers_issues(content, code, +desired_svelte_version, filename);
add_autofixers_issues(content, code, +desired_svelte_version, filename, async);

await add_eslint_issues(content, code, +desired_svelte_version, filename);
await add_eslint_issues(content, code, +desired_svelte_version, filename, async);
} catch (e: unknown) {
const error = e as Error & { start?: { line: number; column: number } };
content.issues.push(
Expand Down