Compare commits

..

9 Commits

Author SHA1 Message Date
paoloricciuti
13719d0646 feat: add derived with functions suggestions 2025-09-18 17:45:51 +02:00
paoloricciuti
4c8cadd585 feat: add imported runes autofixer 2025-09-18 17:07:31 +02:00
paoloricciuti
72e89af60f feat: add set or update stateful variables autofixer 2025-09-18 16:32:25 +02:00
Paolo Ricciuti
13c4832d1b Merge pull request #7 from sveltejs/autofixer-folders-and-tests 2025-09-18 15:42:33 +02:00
paoloricciuti
7c7a1f939f fix: restructure autofixer folders and add tests 2025-09-18 14:54:41 +02:00
Paolo Ricciuti
5becaf3b5e Merge pull request #1 from sveltejs/copy-tables-from-current-mcp 2025-09-17 23:06:36 +02:00
Stanislav Khromov
7f858929ad Merge branch 'main' into copy-tables-from-current-mcp 2025-09-17 22:34:56 +02:00
Stanislav Khromov
6b5588c2f9 Merge pull request #6 from sveltejs/add-devtools
Dev tools docs
2025-09-17 22:34:43 +02:00
paoloricciuti
b4259dd117 chore: copy tables from current mcp 2025-09-11 23:05:09 +02:00
19 changed files with 719 additions and 185 deletions

23
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,23 @@
export const base_runes = [
'$state',
'$effect',
'$derived',
'$inspect',
'$props',
'$bindable',
'$host',
] as const;
export const nested_runes = [
'$state.raw',
'$state.snapshot',
'$effect.pre',
'$effect.tracking',
'$effect.pending',
'$effect.root',
'$derived.by',
'$inspect.trace',
'$props.id',
] as const;
export const runes = [...base_runes, ...nested_runes] as const;

View File

@@ -1,47 +0,0 @@
import type { Node } from 'estree';
import type { AST } from 'svelte-eslint-parser';
import type { Visitors } from 'zimmerframe';
import type { ParseResult } from '../server/analyze/parse.js';
export type Autofixer = Visitors<
Node | AST.SvelteNode,
{
output: { issues: string[]; suggestions: string[] };
parsed: ParseResult;
desired_svelte_version: number;
}
>;
export const assign_in_effect: Autofixer = {
AssignmentExpression(node, { path, state }) {
const in_effect = path.findLast(
(node) =>
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === '$effect',
);
if (
in_effect &&
in_effect.type === 'CallExpression' &&
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
) {
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
if (node.left.type === 'Identifier') {
const reference = state.parsed.find_reference_by_id(node.left);
const definition = reference?.resolved?.defs[0];
if (definition && definition.type === 'Variable') {
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
) {
state.output.suggestions.push(
`The stateful variable "${node.left.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
);
}
}
}
}
}
},
};

View File

@@ -0,0 +1,347 @@
import { describe, expect, it } from 'vitest';
import { add_autofixers_issues } from './add-autofixers-issues.js';
import { base_runes } from '../../constants.js';
const dollarless_runes = base_runes.map((r) => ({ rune: r.replace('$', '') }));
function run_autofixers_on_code(code: string, desired_svelte_version = 5) {
const content = { issues: [], suggestions: [] };
add_autofixers_issues(content, code, desired_svelte_version);
return content;
}
describe('add_autofixers_issues', () => {
describe('assign_in_effect', () => {
it(`should add suggestions when assigning to a stateful variable inside an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
$effect(() => {
count = 43;
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for each variable assigned within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
const count2 = $state(0);
$effect(() => {
count = 43;
count2 = 44;
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
expect(content.suggestions).toContain(
'The stateful variable "count2" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should not add a suggestion for variables that are not assigned within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
</script>
<button onclick={() => count = 43}>Increment</button>
`);
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it("should not add a suggestions for variables that are assigned within an effect but aren't stateful", () => {
const content = run_autofixers_on_code(`
<script>
const count = 0;
$effect(() => {
count = 43;
});
</script>`);
expect(content.suggestions).not.toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for variables that are assigned within an effect with an update`, () => {
const content = run_autofixers_on_code(`
<script>
let count = $state(0);
$effect(() => {
count++;
});
</script>
`);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
it(`should add a suggestion for variables that are mutated within an effect`, () => {
const content = run_autofixers_on_code(`
<script>
let count = $state({ value: 0 });
$effect(() => {
count.value = 42;
});
</script>
`);
expect(content.suggestions).toContain(
'The stateful variable "count" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.',
);
});
});
describe.each([{ method: 'set' }, { method: 'update' }])(
'set_or_update_state ($method)',
({ method }) => {
it(`should add suggestions when using .${method}() on a stateful variable with a literal init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(0);
function update_count() {
count.${method}(43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with an array init`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state([0]);
function update_count() {
count.${method}([1]);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable ($state({}))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state({ value: 0 });
function update_count() {
count.${method}({ value: 43 });
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable ($state(new Class()))`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state(new Class());
function update_count() {
count.${method}(new Class());
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should add suggestions when using .${method}() on a stateful variable with conditional if it's not sure if the method could actually be present on the variable ($state(variable_name))`, () => {
const content = run_autofixers_on_code(`
<script>
const { init } = $props();
const count = $state(init);
function update_count() {
count.${method}(43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
it(`should not add suggestions when using .${method} on a stateful variable if it's not a method call`, () => {
const content = run_autofixers_on_code(`
<script>
const count = $state({});
function update_count() {
console.log(count.${method});
}
</script>`);
expect(content.suggestions).not.toContain(
`You are trying to update the stateful variable "count" using "${method}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them. However I can't verify if "count" is a state variable of an object or a class with a "${method}" method on it. Please verify that before updating the code to use a normal assignment`,
);
});
},
);
describe('imported_runes', () => {
describe.each([{ source: 'svelte' }, { source: 'svelte/runes' }])(
'from "$source"',
({ source }) => {
describe.each(dollarless_runes)('single import ($rune)', ({ rune }) => {
it(`should add suggestions when importing '${rune}' from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { ${rune} } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing "${rune}" as the default export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
it(`should add suggestions when importing '${rune}' as the namespace export from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import * as ${rune} from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
`You are importing "${rune}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${rune}" directly.`,
);
});
});
it(`should add suggestions when importing multiple runes from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount, state, effect } from '${source}';
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(2);
expect(content.suggestions).toContain(
`You are importing "state" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$state" directly.`,
);
expect(content.suggestions).toContain(
`You are importing "effect" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$effect" directly.`,
);
});
it(`should not add suggestions when importing other identifiers from '${source}'`, () => {
const content = run_autofixers_on_code(`
<script>
import { onMount } from '${source}';
</script>`);
expect(content.suggestions).not.toContain(
`You are importing "onMount" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$onMount" directly.`,
);
});
},
);
});
describe('derived_with_function', () => {
it(`should add suggestions when using a function as the first argument to $derived`, () => {
const content = run_autofixers_on_code(`
<script>
const value = $derived(() => {
return 43;
});
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived when declaring "value" but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived in classes`, () => {
const content = run_autofixers_on_code(`
<script>
class Double {
value = $derived(() => 43);
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived when declaring "value" but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived in classes constructors`, () => {
const content = run_autofixers_on_code(`
<script>
class Double {
value;
constructor(){
this.value = $derived(function() { return 44; });
}
}
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived when declaring "value" but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived without the declaring part if it's not an identifier`, () => {
const content = run_autofixers_on_code(`
<script>
const { destructured } = $derived(() => 43);
</script>`);
expect(content.suggestions.length).toBeGreaterThanOrEqual(1);
expect(content.suggestions).toContain(
'You are passing a function to $derived but $derived expects an expression. You can use $derived.by instead.',
);
});
it(`should add suggestions when using a function as the first argument to $derived.by`, () => {
const content = run_autofixers_on_code(`
<script>
const { destructured } = $derived.by(() => 43);
</script>`);
expect(content.suggestions).not.toContain(
'You are passing a function to $derived but $derived expects an expression. You can use $derived.by instead.',
);
});
});
});

View File

@@ -0,0 +1,22 @@
import { parse } from '../../parse/parse.js';
import { walk } from '../../index.js';
import type { Node } from 'estree';
import * as autofixers from './visitors/index.js';
export function add_autofixers_issues(
content: { issues: string[]; suggestions: string[] },
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
) {
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 },
autofixer,
);
}
}

View File

@@ -0,0 +1,20 @@
import { compile } from 'svelte/compiler';
export function add_compile_issues(
content: { issues: string[]; suggestions: string[] },
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
) {
const compilation_result = compile(code, {
filename: filename || 'Component.svelte',
generate: false,
runes: desired_svelte_version >= 5,
});
for (const warning of compilation_result.warnings) {
content.issues.push(
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
);
}
}

View File

@@ -48,7 +48,7 @@ function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
];
}
export function get_linter(version: number) {
function get_linter(version: number) {
if (version < 5) {
return (svelte_4_linter ??= new ESLint({
overrideConfigFile: true,
@@ -68,3 +68,23 @@ export function get_linter(version: number) {
}),
}));
}
export async function add_eslint_issues(
content: { issues: string[]; suggestions: string[] },
code: string,
desired_svelte_version: number,
filename = 'Component.svelte',
) {
const eslint = get_linter(desired_svelte_version);
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
for (const message of results[0].messages) {
if (message.severity === 2) {
content.issues.push(`${message.message} at line ${message.line}, column ${message.column}`);
} else if (message.severity === 1) {
content.suggestions.push(
`${message.message} at line ${message.line}, column ${message.column}`,
);
}
}
}

View File

@@ -0,0 +1,16 @@
import type { Identifier, MemberExpression } from 'estree';
/**
* Gets the left-most identifier of a member expression or identifier.
*/
export function left_most_id(expression: MemberExpression | Identifier) {
while (expression.type === 'MemberExpression') {
expression = expression.object as MemberExpression | Identifier;
}
if (expression.type !== 'Identifier') {
return null;
}
return expression;
}

View File

@@ -0,0 +1,62 @@
import type { AssignmentExpression, Identifier, Node, UpdateExpression } from 'estree';
import type { Autofixer, AutofixerState } from '.';
import { left_most_id } from '../ast/utils.js';
import type { SvelteNode } from 'svelte-eslint-parser/lib/ast';
import type { Context } from 'zimmerframe';
function run_if_in_effect(path: (Node | SvelteNode)[], state: AutofixerState, to_run: () => void) {
const in_effect = path.findLast(
(node) =>
node.type === 'CallExpression' &&
node.callee.type === 'Identifier' &&
node.callee.name === '$effect',
);
if (
in_effect &&
in_effect.type === 'CallExpression' &&
(in_effect.callee.type === 'Identifier' || in_effect.callee.type === 'MemberExpression')
) {
if (state.parsed.is_rune(in_effect, ['$effect', '$effect.pre'])) {
to_run();
}
}
}
function visitor(
node: UpdateExpression | AssignmentExpression,
{ state, path }: Context<Node | SvelteNode, AutofixerState>,
) {
run_if_in_effect(path, state, () => {
function check_if_stateful_id(id: Identifier) {
const reference = state.parsed.find_reference_by_id(id);
const definition = reference?.resolved?.defs[0];
if (definition && definition.type === 'Variable') {
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
) {
state.output.suggestions.push(
`The stateful variable "${id.name}" is assigned inside an $effect which is generally consider a malpractice. Consider using $derived if possible.`,
);
}
}
}
const variable = node.type === 'UpdateExpression' ? node.argument : node.left;
if (variable.type === 'Identifier') {
check_if_stateful_id(variable);
} else if (variable.type === 'MemberExpression') {
const object = left_most_id(variable);
if (object) {
check_if_stateful_id(object);
}
}
});
}
export const assign_in_effect: Autofixer = {
UpdateExpression: visitor,
AssignmentExpression: visitor,
};

View File

@@ -0,0 +1,41 @@
import type { Identifier, PrivateIdentifier } from 'estree';
import type { Autofixer } from '.';
export const derived_with_function: Autofixer = {
CallExpression(node, { state, path }) {
if (
node.callee.type === 'Identifier' &&
node.callee.name === '$derived' &&
state.parsed.is_rune(node, ['$derived']) &&
(node.arguments[0].type === 'ArrowFunctionExpression' ||
node.arguments[0].type === 'FunctionExpression')
) {
const parent = path[path.length - 1];
let variable_id: Identifier | PrivateIdentifier | undefined;
if (parent.type === 'VariableDeclarator' && parent.id.type === 'Identifier') {
// const something = $derived(...)
variable_id = parent.id;
} else if (parent.type === 'PropertyDefinition') {
// class X { something = $derived(...) }
variable_id =
parent.key.type === 'Identifier'
? parent.key
: parent.key.type === 'PrivateIdentifier'
? parent.key
: undefined;
} else if (parent.type === 'AssignmentExpression') {
// this.something = $derived(...)
variable_id =
parent.left.type === 'MemberExpression'
? parent.left.property.type === 'Identifier'
? parent.left.property
: undefined
: undefined;
}
state.output.suggestions.push(
`You are passing a function to $derived ${variable_id ? `when declaring "${variable_id.name}" ` : ''}but $derived expects an expression. You can use $derived.by instead.`,
);
}
},
};

View File

@@ -0,0 +1,28 @@
import { base_runes } from '../../../constants.js';
import type { Autofixer } from '.';
const dollarless_runes = base_runes.map((r) => r.replace('$', ''));
export const imported_runes: Autofixer = {
ImportDeclaration(node, { state, next }) {
const source = (node.source.value || node.source.raw?.slice(1, -1))?.toString();
if (source && source.startsWith('svelte')) {
for (const specifier of node.specifiers) {
const id =
specifier.type === 'ImportDefaultSpecifier'
? specifier.local
: specifier.type === 'ImportNamespaceSpecifier'
? specifier.local
: specifier.type === 'ImportSpecifier'
? specifier.imported
: null;
if (id && id.type === 'Identifier' && dollarless_runes.includes(id.name)) {
state.output.suggestions.push(
`You are importing "${id.name}" from "${source}". This is not necessary, all runes are globally available. Please remove this import and use "$${id.name}" directly.`,
);
}
}
}
next();
},
};

View File

@@ -0,0 +1,17 @@
import type { Node } from 'estree';
import type { AST } from 'svelte-eslint-parser';
import type { Visitors } from 'zimmerframe';
import type { ParseResult } from '../../../parse/parse.js';
export type AutofixerState = {
output: { issues: string[]; suggestions: string[] };
parsed: ParseResult;
desired_svelte_version: number;
};
export type Autofixer = Visitors<Node | AST.SvelteNode, AutofixerState>;
export * from './assign-in-effect.js';
export * from './set-or-update-state.js';
export * from './imported-runes.js';
export * from './derived-with-function.js';

View File

@@ -0,0 +1,37 @@
import type { Autofixer } from '.';
import { left_most_id } from '../ast/utils.js';
const UPDATE_PROPERTIES = ['set', 'update'];
export const set_or_update_state: Autofixer = {
MemberExpression(node, { state, next, path }) {
const parent = path[path.length - 1];
if (
parent.type === 'CallExpression' &&
parent.callee === node &&
node.property.type === 'Identifier' &&
UPDATE_PROPERTIES.includes(node.property.name)
) {
const id = left_most_id(node);
if (id) {
const reference = state.parsed.find_reference_by_id(id);
const definition = reference?.resolved?.defs[0];
if (definition && definition.type === 'Variable') {
const init = definition.node.init;
if (
init?.type === 'CallExpression' &&
state.parsed.is_rune(init, ['$state', '$state.raw'])
) {
let suggestion = `You are trying to update the stateful variable "${id.name}" using "${node.property.name}". stateful variables should be updated with a normal assignment/mutation, do not use methods to update them.`;
const argument = init.arguments[0];
if (!argument || (argument.type !== 'Literal' && argument.type !== 'ArrayExpression')) {
suggestion += ` However I can't verify if "${id.name}" is a state variable of an object or a class with a "${node.property.name}" method on it. Please verify that before updating the code to use a normal assignment`;
}
state.output.suggestions.push(suggestion);
}
}
}
}
next();
},
};

View File

@@ -1,14 +1,11 @@
import { walk } from '../index.js';
import { ValibotJsonSchemaAdapter } from '@tmcp/adapter-valibot';
import { HttpTransport } from '@tmcp/transport-http';
import { StdioTransport } from '@tmcp/transport-stdio';
import type { Node } from 'estree';
import { McpServer } from 'tmcp';
import * as v from 'valibot';
import { parse } from '../server/analyze/parse.js';
import * as autofixers from './autofixers.js';
import { get_linter } from './eslint.js';
import { compile } from 'svelte/compiler';
import { add_autofixers_issues } from './autofixers/add-autofixers-issues.js';
import { add_compile_issues } from './autofixers/add-compile-issues.js';
import { add_eslint_issues } from './autofixers/add-eslint-issues.js';
const server = new McpServer(
{
@@ -64,45 +61,11 @@ server.tool(
require_another_tool_call_after_fixing: boolean;
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
try {
// compile without generating to get warnings and errors
add_compile_issues(content, code, desired_svelte_version, filename);
const compilation_result = compile(code, {
filename: filename || 'Component.svelte',
generate: false,
runes: desired_svelte_version >= 5,
});
add_autofixers_issues(content, code, desired_svelte_version, filename);
for (const warning of compilation_result.warnings) {
content.issues.push(
`${warning.message} at line ${warning.start?.line}, column ${warning.start?.column}`,
);
}
const parsed = parse(code, filename ?? 'Component.svelte');
// 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 },
autofixer,
);
}
const eslint = get_linter(desired_svelte_version);
const results = await eslint.lintText(code, { filePath: filename || './Component.svelte' });
for (const message of results[0].messages) {
if (message.severity === 2) {
content.issues.push(
`${message.message} at line ${message.line}, column ${message.column}`,
);
} else if (message.severity === 1) {
content.suggestions.push(
`${message.message} at line ${message.line}, column ${message.column}`,
);
}
}
await add_eslint_issues(content, code, desired_svelte_version, filename);
} catch (e: unknown) {
const error = e as Error & { start?: { line: number; column: number } };
content.issues.push(

View File

@@ -2,6 +2,7 @@ import ts_parser from '@typescript-eslint/parser';
import type { CallExpression, Identifier } from 'estree';
import type { Reference, Variable } from 'eslint-scope';
import { parseForESLint as svelte_eslint_parse } from 'svelte-eslint-parser';
import { runes } from '../constants.js';
type Scope = {
variables?: Variable[];
@@ -18,25 +19,6 @@ function collect_scopes(scope: Scope, acc: Scope[] = []) {
return acc;
}
const runes = [
'$state',
'$state.raw',
'$state.snapshot',
'$effect',
'$effect.pre',
'$effect.tracking',
'$effect.pending',
'$effect.root',
'$derived',
'$derived.by',
'$inspect',
'$inspect.trace',
'$props',
'$props.id',
'$bindable',
'$host',
] as const;
export type ParseResult = ReturnType<typeof parse>;
export function parse(code: string, file_path: string) {

View File

@@ -12,9 +12,80 @@ import { float_32_array } from './utils';
* to the generated migration file
*/
// this is just an example of a vector table...we can change this with the docs table later
export const vector_table = sqliteTable('vector_table', {
export const distillations = sqliteTable('distillations', {
id: integer('id').primaryKey(),
text: text('text'),
vector: float_32_array('vector', { dimensions: 3 }),
preset_name: text('preset_name').notNull(),
version: text('version').notNull(),
content: text('content').notNull(),
size_kb: integer('size_kb').notNull(),
document_count: integer('document_count').notNull(),
distillation_job_id: integer('distillation_job_id').references(() => distillation_jobs.id),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const distillation_jobs = sqliteTable('distillation_jobs', {
id: integer('id').primaryKey(),
preset_name: text('preset_name').notNull(),
batch_id: text('batch_id'),
status: text('status', { enum: ['pending', 'processing', 'completed', 'failed'] }).notNull(),
model_used: text('model_used').notNull(),
total_files: integer('total_files').notNull(),
processed_files: integer('processed_files').notNull().default(0),
successful_files: integer('successful_files').notNull().default(0),
minimize_applied: integer('minimize_applied', { mode: 'boolean' }).notNull().default(false),
total_input_tokens: integer('total_input_tokens').notNull().default(0),
total_output_tokens: integer('total_output_tokens').notNull().default(0),
started_at: integer('started_at', { mode: 'timestamp' }),
completed_at: integer('completed_at', { mode: 'timestamp' }),
error_message: text('error_message'),
metadata: text('metadata', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull()
.default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content = sqliteTable('content', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull()
.default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});
export const content_distilled = sqliteTable('content_distilled', {
id: integer('id').primaryKey(),
path: text('path').notNull(),
filename: text('filename').notNull(),
content: text('content').notNull(),
size_bytes: integer('size_bytes').notNull(),
embeddings: float_32_array('embeddings', { dimensions: 1024 }),
metadata: text('metadata', { mode: 'json' })
.$type<Record<string, unknown>>()
.notNull()
.default({}),
created_at: integer('created_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
updated_at: integer('updated_at', { mode: 'timestamp' })
.notNull()
.$defaultFn(() => new Date()),
});

View File

@@ -26,7 +26,9 @@ export function vector(arr: number[]) {
* .execute();
*/
export function distance(column: Column, arr: number[], as = 'distance') {
return sql<number>`vector_distance_cos(${column}, vector32(${JSON.stringify(arr)}))`.as(as);
return sql<number>`CASE ${column} ISNULL WHEN 1 THEN 1 ELSE vector_distance_cos(${column}, vector32(${JSON.stringify(arr)})) END`.as(
as,
);
}
/**

View File

@@ -1,50 +0,0 @@
import { VOYAGE_API_KEY } from '$env/static/private';
import { db } from '$lib/server/db/index.js';
import { vector_table } from '$lib/server/db/schema.js';
import { distance, vector } from '$lib/server/db/utils.js';
import { sql } from 'drizzle-orm';
async function get_embeddings(text: string) {
const result = await fetch('https://api.voyageai.com/v1/embeddings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${VOYAGE_API_KEY}`,
},
body: JSON.stringify({
input: [text],
model: 'voyage-3.5',
}),
}).then((res) => res.json());
return result.data[0].embedding as number[];
}
export async function load({ url: { searchParams } }) {
const sentence = searchParams.get('sentence');
if (!sentence) return { top: [], sentence: '' };
const top = await db
.select({
id: vector_table.id,
text: vector_table.text,
distance: distance(vector_table.vector, await get_embeddings(sentence)),
})
.from(vector_table)
.orderBy(sql`distance`)
.execute();
return { top, sentence };
}
export const actions = {
async default({ request }) {
const data = await request.formData();
const text = data.get('text')?.toString();
const embeddings = await get_embeddings(text ?? '');
if (text && embeddings) {
await db
.insert(vector_table)
.values({ text, vector: vector(embeddings) })
.execute();
}
},
};

View File

@@ -1,21 +1 @@
<script lang="ts">
import { enhance } from '$app/forms';
let { data } = $props();
</script>
<h1>Official Svelte MCP</h1>
<form method="POST" use:enhance>
<textarea name="text" rows="4" cols="50" placeholder="Enter text to store in the vector database"
></textarea>
<br />
<button>Submit</button>
</form>
Comparing with
<pre>{data.sentence}</pre>
{#each data.top as item (item.id)}
<p>{item.text} - {item.distance}</p>
{/each}