From 6d803dd76218da94b68327310420ddc0723c77e5 Mon Sep 17 00:00:00 2001 From: Yhvr Date: Tue, 28 Jul 2026 23:30:10 -0400 Subject: [PATCH] fix(language-server): strip AstroComponent suffix from Svelte and Vue auto-imports --- .changeset/sunny-buttons-call.md | 6 +++ .../src/plugins/typescript/completions.ts | 21 +++++----- .../src/plugins/typescript/utils.ts | 16 ++++---- .../language-server/test/units/utils.test.ts | 38 +++++++++---------- 4 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 .changeset/sunny-buttons-call.md diff --git a/.changeset/sunny-buttons-call.md b/.changeset/sunny-buttons-call.md new file mode 100644 index 000000000000..11696b456914 --- /dev/null +++ b/.changeset/sunny-buttons-call.md @@ -0,0 +1,6 @@ +--- +'@astrojs/language-server': patch +'astro-vscode': patch +--- + +Fixes AstroComponent suffix not being removed from Svelte and Vue imports in .astro files diff --git a/packages/language-tools/language-server/src/plugins/typescript/completions.ts b/packages/language-tools/language-server/src/plugins/typescript/completions.ts index d52b977ff9d8..96c280fe2b32 100644 --- a/packages/language-tools/language-server/src/plugins/typescript/completions.ts +++ b/packages/language-tools/language-server/src/plugins/typescript/completions.ts @@ -7,10 +7,10 @@ import { CompletionItemKind } from '@volar/language-server'; import { URI } from 'vscode-uri'; import { AstroVirtualCode } from '../../core/index.js'; import { - getAlreadyImportedAstroComponentSources, - isAstroComponentImportSource, + getAlreadyImportedComponentSources, + isComponentImportSource, mapEdit, - rewriteAstroImportText, + rewriteComponentImportText, stripAstroComponentSuffix, } from './utils.js'; @@ -19,7 +19,7 @@ export function enhancedProvideCompletionItems( completions: CompletionList, documentText: string, ): CompletionList { - const importedAstroSources = getAlreadyImportedAstroComponentSources(ts, documentText); + const importedComponentSources = getAlreadyImportedComponentSources(ts, documentText); completions.items = completions.items .filter((completion) => { @@ -28,7 +28,7 @@ export function enhancedProvideCompletionItems( } const source = completion?.data?.originalItem?.source; - return !(source && importedAstroSources.has(source)); + return !(source && importedComponentSources.has(source)); }) .map((completion) => { const source = completion?.data?.originalItem?.source; @@ -39,15 +39,12 @@ export function enhancedProvideCompletionItems( } // For components import, use the file kind and sort them first, as they're often what the user want over something else - if (['.astro', '.svelte', '.vue'].some((ext) => source.endsWith(ext))) { + if (isComponentImportSource(source)) { completion.kind = CompletionItemKind.File; completion.detail = completion.detail + '\n\n' + source; completion.sortText = '\u0001' + (completion.sortText ?? completion.label); completion.data.isComponent = true; - - if (isAstroComponentImportSource(source)) { - rewriteAstroComponentCompletion(completion); - } + rewriteAstroComponentCompletion(completion); } } @@ -69,7 +66,7 @@ export function enhancedResolveCompletionItem( ); } - if (isAstroComponentImportSource(resolvedCompletion.data.originalItem.source)) { + if (isComponentImportSource(resolvedCompletion.data.originalItem.source)) { rewriteAstroComponentCompletion(resolvedCompletion); } @@ -104,7 +101,7 @@ function rewriteAstroComponentCompletion(completion: CompletionItem) { if (completion.additionalTextEdits) { completion.additionalTextEdits = completion.additionalTextEdits.map((edit) => ({ ...edit, - newText: rewriteAstroImportText(edit.newText), + newText: rewriteComponentImportText(edit.newText), })); } } diff --git a/packages/language-tools/language-server/src/plugins/typescript/utils.ts b/packages/language-tools/language-server/src/plugins/typescript/utils.ts index ba9fcda4f8de..5224fb0ba9d3 100644 --- a/packages/language-tools/language-server/src/plugins/typescript/utils.ts +++ b/packages/language-tools/language-server/src/plugins/typescript/utils.ts @@ -3,13 +3,13 @@ import type { AstroVirtualCode } from '../../core/index.js'; import { editShouldBeInFrontmatter, ensureProperEditForFrontmatter } from '../utils.js'; const ASTRO_COMPONENT_SUFFIX = 'AstroComponent'; -const ASTRO_IMPORT_FROM_PATTERN = /\bfrom\s+['"][^'"]+\.astro['"]/; +const COMPONENT_IMPORT_FROM_PATTERN = /\bfrom\s+['"][^'"]+\.(?:astro|svelte|vue)['"]/; const ASTRO_DEFAULT_IMPORT_PATTERN = /^(\s*import(?:\s+type)?\s+)([A-Za-z_$][\w$]*)AstroComponent(?=\s*,|\s+from\b)/; const ASTRO_DEFAULT_ALIAS_PATTERN = /(default\s+as\s+)([A-Za-z_$][\w$]*)AstroComponent(?=\s*\})/; -export function isAstroComponentImportSource(source: string | undefined): source is string { - return !!source && source.endsWith('.astro'); +export function isComponentImportSource(source: string | undefined): source is string { + return !!source && (source.endsWith('.astro') || source.endsWith('.svelte') || source.endsWith('.vue')); } export function stripAstroComponentSuffix(name: string) { @@ -20,11 +20,11 @@ export function stripAstroComponentSuffix(name: string) { return name.slice(0, -ASTRO_COMPONENT_SUFFIX.length); } -export function rewriteAstroImportText(text: string) { +export function rewriteComponentImportText(text: string) { return text .split('\n') .map((line) => { - if (!ASTRO_IMPORT_FROM_PATTERN.test(line)) { + if (!COMPONENT_IMPORT_FROM_PATTERN.test(line)) { return line; } @@ -35,7 +35,7 @@ export function rewriteAstroImportText(text: string) { .join('\n'); } -export function getAlreadyImportedAstroComponentSources( +export function getAlreadyImportedComponentSources( ts: typeof import('typescript'), documentText: string, ) { @@ -55,7 +55,7 @@ export function getAlreadyImportedAstroComponentSources( const source = statement.moduleSpecifier.text; const importClause = statement.importClause; - if (!importClause || importClause.isTypeOnly || !isAstroComponentImportSource(source)) { + if (!importClause || importClause.isTypeOnly || !isComponentImportSource(source)) { continue; } @@ -108,7 +108,7 @@ export function mapEdit(edit: TextEdit, code: AstroVirtualCode, languageId: stri } } - edit.newText = rewriteAstroImportText(edit.newText); + edit.newText = rewriteComponentImportText(edit.newText); return edit; } diff --git a/packages/language-tools/language-server/test/units/utils.test.ts b/packages/language-tools/language-server/test/units/utils.test.ts index 55750f03a3b1..7af93d5e21a4 100644 --- a/packages/language-tools/language-server/test/units/utils.test.ts +++ b/packages/language-tools/language-server/test/units/utils.test.ts @@ -9,8 +9,8 @@ import { addAstroTypes } from '../../dist/core/index.js'; import { getAstroMetadata } from '../../dist/core/parseAstro.js'; import { patchTSX } from '../../dist/core/utils.js'; import { - getAlreadyImportedAstroComponentSources, - rewriteAstroImportText, + getAlreadyImportedComponentSources, + rewriteComponentImportText, } from '../../dist/plugins/typescript/utils.js'; import * as utils from '../../dist/plugins/utils.js'; @@ -127,54 +127,54 @@ describe('Utilities', async () => { }); }); - it('rewriteAstroImportText - strips AstroComponent suffixes from default Astro imports', () => { + it('rewriteComponentImportText - strips AstroComponent suffixes from component imports', () => { assert.strictEqual( - rewriteAstroImportText(`import ImageAstroComponent from "../components/Image.astro";\n`), - `import Image from "../components/Image.astro";\n`, + rewriteComponentImportText(`import ImageAstroComponent from "../components/Image.astro";\nimport CardAstroComponent from "../components/Card.vue";\nimport ChipAstroComponent from "../components/Chip.svelte";\n`), + `import Image from "../components/Image.astro";\nimport Card from "../components/Card.vue";\nimport Chip from "../components/Chip.svelte";\n`, ); }); - it('rewriteAstroImportText - only rewrites Astro imports', () => { + it('rewriteComponentImportText - only rewrites component imports', () => { assert.strictEqual( - rewriteAstroImportText(`import ImageAstroComponent from "astro:assets";\n`), + rewriteComponentImportText(`import ImageAstroComponent from "astro:assets";\n`), `import ImageAstroComponent from "astro:assets";\n`, ); }); - it('rewriteAstroImportText - preserves named imports on Astro component imports', () => { + it('rewriteComponentImportText - preserves named imports on component imports', () => { assert.strictEqual( - rewriteAstroImportText( + rewriteComponentImportText( `import ImageAstroComponent, { type Props } from "../components/Image.astro";\n`, ), `import Image, { type Props } from "../components/Image.astro";\n`, ); }); - it('rewriteAstroImportText - strips AstroComponent suffixes from default aliases', () => { + it('rewriteComponentImportText - strips AstroComponent suffixes from default aliases', () => { assert.strictEqual( - rewriteAstroImportText( + rewriteComponentImportText( `import { default as ImageAstroComponent } from "../components/Image.astro";\n`, ), `import { default as Image } from "../components/Image.astro";\n`, ); }); - it('getAlreadyImportedAstroComponentSources - detects runtime Astro component imports', () => { + it('getAlreadyImportedComponentSources - detects runtime component imports', () => { assert.deepStrictEqual( Array.from( - getAlreadyImportedAstroComponentSources( + getAlreadyImportedComponentSources( ts, - `import Image from "../components/Image.astro";\nimport { default as Card } from "../components/Card.astro";\n`, + `import Image from "../components/Image.astro";\nimport { default as Card } from "../components/Card.vue";\nimport ChipComp from "../components/Chip.svelte";\n`, ), ), - ['../components/Image.astro', '../components/Card.astro'], + ['../components/Image.astro', '../components/Card.vue', '../components/Chip.svelte'], ); }); - it('getAlreadyImportedAstroComponentSources - ignores type-only Astro imports', () => { + it('getAlreadyImportedComponentSources - ignores type-only Astro imports', () => { assert.deepStrictEqual( Array.from( - getAlreadyImportedAstroComponentSources( + getAlreadyImportedComponentSources( ts, `import type { Props } from "../components/Image.astro";\n`, ), @@ -183,10 +183,10 @@ describe('Utilities', async () => { ); }); - it('getAlreadyImportedAstroComponentSources - parses Astro frontmatter imports', () => { + it('getAlreadyImportedComponentSources - parses Astro frontmatter imports', () => { assert.deepStrictEqual( Array.from( - getAlreadyImportedAstroComponentSources( + getAlreadyImportedComponentSources( ts, `--- import Image from "../components/Image.astro";