mirror of
https://github.com/sveltejs/ai-tools.git
synced 2026-07-04 11:42:22 +08:00
Compare commits
26 Commits
ci
...
eslint-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d81d6a3d95 | ||
|
|
f04cb139e3 | ||
|
|
fd355e872d | ||
|
|
d4edef1d82 | ||
|
|
26d30c73c7 | ||
|
|
13719d0646 | ||
|
|
4c8cadd585 | ||
|
|
72e89af60f | ||
|
|
13c4832d1b | ||
|
|
7c7a1f939f | ||
|
|
5becaf3b5e | ||
|
|
7f858929ad | ||
|
|
6b5588c2f9 | ||
|
|
684d7effef | ||
|
|
a0f8abb632 | ||
|
|
f85e4612ac | ||
|
|
11aceaf047 | ||
|
|
e515b9eaf9 | ||
|
|
ffafb8a492 | ||
|
|
cba6ab8f4b | ||
|
|
052331a80e | ||
|
|
887137e794 | ||
|
|
037f830557 | ||
|
|
7c3eb030ce | ||
|
|
2ded5711fb | ||
|
|
b4259dd117 |
@@ -1,2 +1,3 @@
|
||||
DATABASE_URL=file:test.db
|
||||
DATABASE_TOKEN=needs_to_be_set_but_it_can_be_anything
|
||||
VOYAGE_API_KEY=your_actual_api_key_here
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,3 +24,4 @@ vite.config.ts.timestamp-*
|
||||
|
||||
# SQLite
|
||||
*.db
|
||||
dist
|
||||
10
.vscode/mcp.json
vendored
Normal file
10
.vscode/mcp.json
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"servers": {
|
||||
"Svelte MCP": {
|
||||
"type": "stdio",
|
||||
"command": "node",
|
||||
"args": ["dist/lib/stdio.js"]
|
||||
}
|
||||
},
|
||||
"inputs": []
|
||||
}
|
||||
21
README.md
21
README.md
@@ -11,3 +11,24 @@ pnpm dev
|
||||
```
|
||||
|
||||
1. Set the VOYAGE_API_KEY for embeddings support
|
||||
|
||||
### Local dev tools
|
||||
|
||||
#### MCP inspector
|
||||
|
||||
```
|
||||
pnpm run inspect
|
||||
```
|
||||
|
||||
Then visit http://localhost:6274/
|
||||
|
||||
- Transport type: `Streamable HTTP`
|
||||
- http://localhost:5173/mcp
|
||||
|
||||
#### Database inspector
|
||||
|
||||
```
|
||||
pnpm run db:studio
|
||||
```
|
||||
|
||||
https://local.drizzle.studio/
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
if (!process.env.DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
|
||||
|
||||
export default defineConfig({
|
||||
schema: './src/lib/server/db/schema.ts',
|
||||
dialect: 'sqlite',
|
||||
dbCredentials: { url: process.env.DATABASE_URL },
|
||||
dialect: 'turso',
|
||||
dbCredentials: { url: process.env.DATABASE_URL, authToken: process.env.DATABASE_TOKEN },
|
||||
verbose: true,
|
||||
strict: true,
|
||||
});
|
||||
|
||||
@@ -7,10 +7,10 @@ import { fileURLToPath } from 'node:url';
|
||||
import ts from 'typescript-eslint';
|
||||
import svelteConfig from './svelte.config.js';
|
||||
|
||||
const gitignorePath = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
const gitignore_path = fileURLToPath(new URL('./.gitignore', import.meta.url));
|
||||
|
||||
export default ts.config(
|
||||
includeIgnoreFile(gitignorePath),
|
||||
includeIgnoreFile(gitignore_path),
|
||||
js.configs.recommended,
|
||||
...ts.configs.recommended,
|
||||
...svelte.configs.recommended,
|
||||
@@ -24,6 +24,15 @@ export default ts.config(
|
||||
// typescript-eslint strongly recommend that you do not use the no-undef lint rule on TypeScript projects.
|
||||
// see: https://typescript-eslint.io/troubleshooting/faqs/eslint/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
'no-undef': 'off',
|
||||
'@typescript-eslint/naming-convention': [
|
||||
'error',
|
||||
{
|
||||
selector: ['variableLike'],
|
||||
format: ['snake_case', 'UPPER_CASE'],
|
||||
leadingUnderscore: 'allow',
|
||||
},
|
||||
],
|
||||
'func-style': ['error', 'declaration', { allowTypeAnnotation: true }],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
10
package.json
10
package.json
@@ -4,10 +4,15 @@
|
||||
"description": "The official Svelte MCP server implementation",
|
||||
"type": "module",
|
||||
"main": "src/index.js",
|
||||
"bin": {
|
||||
"svelte-mcp": "./dist/lib/stdio.js"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build:mcp": "tsc --project tsconfig.build.json",
|
||||
"prepublishOnly": "pnpm build:mcp",
|
||||
"preview": "vite preview",
|
||||
"prepare": "svelte-kit sync || echo ''",
|
||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||
@@ -21,7 +26,8 @@
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:studio": "drizzle-kit studio"
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"inspect": "DANGEROUSLY_OMIT_AUTH=true npx @modelcontextprotocol/inspector"
|
||||
},
|
||||
"keywords": [
|
||||
"svelte",
|
||||
@@ -34,6 +40,7 @@
|
||||
"@eslint/compat": "^1.2.5",
|
||||
"@eslint/js": "^9.18.0",
|
||||
"@libsql/client": "^0.14.0",
|
||||
"@modelcontextprotocol/inspector": "^0.16.7",
|
||||
"@sveltejs/adapter-vercel": "^5.6.3",
|
||||
"@sveltejs/kit": "^2.22.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||
@@ -41,6 +48,7 @@
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/node": "^24.3.1",
|
||||
"@typescript-eslint/types": "^8.43.0",
|
||||
"dotenv": "^17.2.2",
|
||||
"drizzle-kit": "^0.30.2",
|
||||
"drizzle-orm": "^0.40.0",
|
||||
"eslint": "^9.18.0",
|
||||
|
||||
1992
pnpm-lock.yaml
generated
1992
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
23
src/lib/constants.ts
Normal file
23
src/lib/constants.ts
Normal 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;
|
||||
@@ -1,6 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { walk as _walk } from 'zimmerframe';
|
||||
import type { Autofixer } from './mcp/autofixers';
|
||||
|
||||
export type WalkParams<
|
||||
T extends {
|
||||
@@ -31,25 +30,3 @@ export function walk<
|
||||
...visitors,
|
||||
});
|
||||
}
|
||||
|
||||
export function mix_visitors(autofixers: Record<string, Autofixer>): Autofixer {
|
||||
const visitors: Autofixer = {};
|
||||
for (const key in autofixers) {
|
||||
const new_visitors = autofixers[key];
|
||||
for (const node_type in new_visitors) {
|
||||
if (node_type in visitors) {
|
||||
const existing_visitor = visitors[node_type as keyof typeof visitors]!;
|
||||
const new_visitor = new_visitors[node_type as keyof typeof visitors]!;
|
||||
visitors[node_type as keyof typeof visitors] = (node, ctx) => {
|
||||
(existing_visitor as any)(node, ctx);
|
||||
(new_visitor as any)(node, ctx);
|
||||
};
|
||||
} else {
|
||||
visitors[node_type as keyof typeof visitors] = new_visitors[
|
||||
node_type as keyof typeof visitors
|
||||
] as never;
|
||||
}
|
||||
}
|
||||
}
|
||||
return visitors;
|
||||
}
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import type { Node } from 'estree';
|
||||
import type { Visitors } from 'zimmerframe';
|
||||
import type { ParseResult } from '$lib/server/analyze/parse.js';
|
||||
|
||||
export type Autofixer = Visitors<
|
||||
Node,
|
||||
{ output: { issues: string[]; suggestions: string[] }; parsed: ParseResult }
|
||||
>;
|
||||
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
347
src/lib/mcp/autofixers/add-autofixers-issues.test.ts
Normal file
347
src/lib/mcp/autofixers/add-autofixers-issues.test.ts
Normal 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.',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
22
src/lib/mcp/autofixers/add-autofixers-issues.ts
Normal file
22
src/lib/mcp/autofixers/add-autofixers-issues.ts
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
20
src/lib/mcp/autofixers/add-compile-issues.ts
Normal file
20
src/lib/mcp/autofixers/add-compile-issues.ts
Normal 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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
90
src/lib/mcp/autofixers/add-eslint-issues.ts
Normal file
90
src/lib/mcp/autofixers/add-eslint-issues.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { ESLint } from 'eslint';
|
||||
import svelte_parser from 'svelte-eslint-parser';
|
||||
import svelte from 'eslint-plugin-svelte';
|
||||
import type { Config } from '@sveltejs/kit';
|
||||
|
||||
let svelte_5_linter: ESLint | undefined;
|
||||
|
||||
let svelte_4_linter: ESLint | undefined;
|
||||
|
||||
function base_config(svelte_config: Config): ESLint.Options['baseConfig'] {
|
||||
return [
|
||||
...svelte.configs.recommended,
|
||||
{
|
||||
files: ['*.svelte'],
|
||||
rules: {
|
||||
'no-self-assign': 'warn',
|
||||
'svelte/infinite-reactive-loop': 'warn',
|
||||
'svelte/no-dupe-else-if-blocks': 'warn',
|
||||
'svelte/no-dupe-on-directives': 'warn',
|
||||
'svelte/no-dupe-style-properties': 'warn',
|
||||
'svelte/no-dupe-use-directives': 'warn',
|
||||
'svelte/no-object-in-text-mustaches': 'warn',
|
||||
'svelte/no-raw-special-elements': 'warn',
|
||||
'svelte/no-reactive-functions': 'warn',
|
||||
'svelte/no-reactive-literals': 'warn',
|
||||
'svelte/no-store-async': 'warn',
|
||||
'svelte/no-svelte-internal': 'warn',
|
||||
'svelte/no-unnecessary-state-wrap': 'warn',
|
||||
'svelte/no-unused-props': 'warn',
|
||||
'svelte/no-unused-svelte-ignore': 'warn',
|
||||
'svelte/no-useless-children-snippet': 'warn',
|
||||
'svelte/no-useless-mustaches': 'warn',
|
||||
'svelte/prefer-svelte-reactivity': 'warn',
|
||||
'svelte/prefer-writable-derived': 'warn',
|
||||
'svelte/require-event-dispatcher-types': 'warn',
|
||||
'svelte/require-store-reactive-access': 'warn',
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
parser: svelte_parser,
|
||||
parserOptions: {
|
||||
svelteConfig: svelte_config,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function get_linter(version: number) {
|
||||
if (version < 5) {
|
||||
return (svelte_4_linter ??= new ESLint({
|
||||
overrideConfigFile: true,
|
||||
baseConfig: base_config({
|
||||
compilerOptions: {
|
||||
runes: false,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
}
|
||||
return (svelte_5_linter ??= new ESLint({
|
||||
overrideConfigFile: true,
|
||||
baseConfig: base_config({
|
||||
compilerOptions: {
|
||||
runes: true,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
src/lib/mcp/autofixers/ast/utils.ts
Normal file
16
src/lib/mcp/autofixers/ast/utils.ts
Normal 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;
|
||||
}
|
||||
62
src/lib/mcp/autofixers/visitors/assign-in-effect.ts
Normal file
62
src/lib/mcp/autofixers/visitors/assign-in-effect.ts
Normal 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,
|
||||
};
|
||||
41
src/lib/mcp/autofixers/visitors/derived-with-function.ts
Normal file
41
src/lib/mcp/autofixers/visitors/derived-with-function.ts
Normal 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.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
28
src/lib/mcp/autofixers/visitors/imported-runes.ts
Normal file
28
src/lib/mcp/autofixers/visitors/imported-runes.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
17
src/lib/mcp/autofixers/visitors/index.ts
Normal file
17
src/lib/mcp/autofixers/visitors/index.ts
Normal 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';
|
||||
37
src/lib/mcp/autofixers/visitors/set-or-update-state.ts
Normal file
37
src/lib/mcp/autofixers/visitors/set-or-update-state.ts
Normal 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();
|
||||
},
|
||||
};
|
||||
@@ -1,12 +1,11 @@
|
||||
import { mix_visitors, walk } from '$lib';
|
||||
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 { 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(
|
||||
{
|
||||
@@ -23,7 +22,7 @@ const server = new McpServer(
|
||||
completions: {},
|
||||
},
|
||||
instructions:
|
||||
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code',
|
||||
'This is the official Svelte MCP server. It MUST be used whenever svelte development is involved. It can provide official documentation, code examples and correct your code. After you correct the component call this tool again to confirm all the issues are fixed.',
|
||||
},
|
||||
);
|
||||
|
||||
@@ -35,11 +34,18 @@ server.tool(
|
||||
'Given a svelte component or module returns a list of suggestions to fix any issues it has. This tool MUST be used whenever the user is asking to write svelte code before sending the code back to the user',
|
||||
schema: v.object({
|
||||
code: v.string(),
|
||||
desired_svelte_version: v.pipe(
|
||||
v.union([v.literal(4), v.literal(5)]),
|
||||
v.description(
|
||||
'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.',
|
||||
),
|
||||
),
|
||||
filename: v.optional(v.string()),
|
||||
}),
|
||||
outputSchema: v.object({
|
||||
issues: v.optional(v.array(v.string())),
|
||||
suggestions: v.optional(v.array(v.string())),
|
||||
issues: v.array(v.string()),
|
||||
suggestions: v.array(v.string()),
|
||||
require_another_tool_call_after_fixing: v.boolean(),
|
||||
}),
|
||||
annotations: {
|
||||
title: 'Svelte Autofixer',
|
||||
@@ -48,12 +54,28 @@ server.tool(
|
||||
openWorldHint: false,
|
||||
},
|
||||
},
|
||||
async ({ code, filename }) => {
|
||||
const content: { issues: string[]; suggestions: string[] } = { issues: [], suggestions: [] };
|
||||
async ({ code, filename, desired_svelte_version }) => {
|
||||
const content: {
|
||||
issues: string[];
|
||||
suggestions: string[];
|
||||
require_another_tool_call_after_fixing: boolean;
|
||||
} = { issues: [], suggestions: [], require_another_tool_call_after_fixing: false };
|
||||
try {
|
||||
add_compile_issues(content, code, desired_svelte_version, filename);
|
||||
|
||||
const parsed = parse(code, filename ?? 'Component.svelte');
|
||||
add_autofixers_issues(content, code, desired_svelte_version, filename);
|
||||
|
||||
walk(parsed.ast as unknown as Node, { output: content, parsed }, mix_visitors(autofixers));
|
||||
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(
|
||||
`${error.message} at line ${error.start?.line}, column ${error.start?.column}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (content.issues.length > 0 || content.suggestions.length > 0) {
|
||||
content.require_another_tool_call_after_fixing = true;
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
@@ -67,6 +89,46 @@ server.tool(
|
||||
},
|
||||
);
|
||||
|
||||
server.prompt(
|
||||
{
|
||||
name: 'svelte-task-prompt',
|
||||
title: 'Svelte Task Prompt',
|
||||
description:
|
||||
'Use this Prompt to ask for any svelte related task. It will automatically instruct the LLM on how to best use the autofixer and how to query for documentation pages.',
|
||||
schema: v.object({
|
||||
task: v.pipe(v.string(), v.description('The task to be performed')),
|
||||
}),
|
||||
},
|
||||
async ({ task }) => {
|
||||
// TODO: implement logic to fetch the available docs paths to return in the prompt
|
||||
const available_docs: string[] = [];
|
||||
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `You are a Svelte expert tasked to build components and utilities for Svelte developers. If you need documentation for anything related to Svelte you can invoke the tool \`get_documentation\` with one of the following paths:
|
||||
<available-docs-paths>
|
||||
${JSON.stringify(available_docs, null, 2)}
|
||||
</available-docs-paths>
|
||||
|
||||
Every time you write a Svelte component or a Svelte module you MUST invoke the \`svelte-autofixer\` tool providing the code. The tool will return a list of issues or suggestions. If there are any issues or suggestions you MUST fix them and call the tool again with the updated code. You MUST keep doing this until the tool returns no issues or suggestions. Only then you can return the code to the user.
|
||||
|
||||
This is the task you will work on:
|
||||
|
||||
<task>
|
||||
${task}
|
||||
</task>
|
||||
`,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
export const http_transport = new HttpTransport(server, {
|
||||
cors: true,
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
@@ -1,10 +1,13 @@
|
||||
import { DATABASE_URL } from '$env/static/private';
|
||||
import { createClient } from '@libsql/client';
|
||||
import { drizzle } from 'drizzle-orm/libsql';
|
||||
import * as schema from './schema';
|
||||
|
||||
if (!DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
if (!process.env.DATABASE_URL) throw new Error('DATABASE_URL is not set');
|
||||
if (!process.env.DATABASE_TOKEN) throw new Error('DATABASE_TOKEN is not set');
|
||||
|
||||
const client = createClient({ url: DATABASE_URL });
|
||||
const client = createClient({
|
||||
url: process.env.DATABASE_URL,
|
||||
authToken: process.env.DATABASE_TOKEN,
|
||||
});
|
||||
|
||||
export const db = drizzle(client, { schema, logger: true });
|
||||
|
||||
@@ -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()),
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
4
src/lib/stdio.ts
Normal file
4
src/lib/stdio.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env node
|
||||
import { stdio_transport } from './mcp/index.js';
|
||||
|
||||
stdio_transport.listen();
|
||||
@@ -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();
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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}
|
||||
|
||||
21
tsconfig.build.json
Normal file
21
tsconfig.build.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": true,
|
||||
"checkJs": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"skipLibCheck": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/lib/stdio.ts", "src/lib/mcp/**/*"],
|
||||
"exclude": ["node_modules", "dist", "src/routes/**/*", "src/app.html", "src/hooks.server.ts"]
|
||||
}
|
||||
@@ -1,21 +1,26 @@
|
||||
import devtoolsJson from 'vite-plugin-devtools-json';
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import { config } from 'dotenv';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit(), devtoolsJson()],
|
||||
test: {
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
{
|
||||
extends: './vite.config.ts',
|
||||
test: {
|
||||
name: 'server',
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
export default defineConfig(({ mode }) => {
|
||||
config({ path: ['.env', `.env.${mode}`] });
|
||||
|
||||
return {
|
||||
plugins: [sveltekit(), devtoolsJson()],
|
||||
test: {
|
||||
expect: { requireAssertions: true },
|
||||
projects: [
|
||||
{
|
||||
extends: './vite.config.ts',
|
||||
test: {
|
||||
name: 'server',
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,ts}'],
|
||||
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user