From 5456e53a0ae1e8e8959be07554e10c38088c221f Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Wed, 16 Apr 2025 05:09:38 +0800 Subject: [PATCH 1/7] fix: various hoistability check (#2740) #2671 #2727 --- .../svelte2tsx/nodes/HoistableInterfaces.ts | 49 ++++++++++++++++--- .../expectedv2.ts | 19 +++++++ .../input.svelte | 10 ++++ .../expectedv2.ts | 17 +++++++ .../input.svelte | 9 ++++ .../expectedv2.ts | 20 ++++++++ .../input.svelte | 13 +++++ .../expectedv2.ts | 20 ++++++++ .../input.svelte | 13 +++++ .../expectedv2.ts | 20 ++++++++ .../input.svelte | 13 +++++ .../expectedv2.ts | 19 +++++++ .../input.svelte | 12 +++++ .../expectedv2.ts | 36 ++++++++++++++ .../input.svelte | 10 ++++ .../expectedv2.ts | 31 ++++++++++++ .../input.svelte | 5 ++ 17 files changed, 309 insertions(+), 7 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts index 7d95d1c73..dd2ebaceb 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/HoistableInterfaces.ts @@ -86,6 +86,14 @@ export class HoistableInterfaces { if (ts.isInterfaceDeclaration(node)) { this.module_types.add(node.name.text); } + + if (ts.isEnumDeclaration(node)) { + this.module_types.add(node.name.text); + } + + if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) { + this.module_types.add(node.name.text); + } } analyzeInstanceScriptNode(node: ts.Node) { @@ -158,6 +166,24 @@ export class HoistableInterfaces { } }); + node.heritageClauses?.forEach((clause) => { + clause.types.forEach((type) => { + if (ts.isIdentifier(type.expression)) { + const type_name = type.expression.text; + if (!generics.includes(type_name)) { + type_dependencies.add(type_name); + } + } + + this.collectTypeDependencies( + type, + type_dependencies, + value_dependencies, + generics + ); + }); + }); + if (this.module_types.has(interface_name)) { // shadowed; we can't hoist this.disallowed_types.add(interface_name); @@ -229,6 +255,14 @@ export class HoistableInterfaces { if (ts.isEnumDeclaration(node)) { this.disallowed_values.add(node.name.text); } + + // namespace declaration should not be in the instance script. + // Only adding the top-level name to the disallowed list, + // so that at least there won't a confusing error message of "can't find namespace Foo" + if (ts.isModuleDeclaration(node) && ts.isIdentifier(node.name)) { + this.disallowed_types.add(node.name.text); + this.disallowed_values.add(node.name.text); + } } analyze$propsRune( @@ -239,7 +273,7 @@ export class HoistableInterfaces { if (node.initializer.typeArguments?.length > 0 || node.type) { const generic_arg = node.initializer.typeArguments?.[0] || node.type; if (ts.isTypeReferenceNode(generic_arg)) { - const name = this.getEntityNameText(generic_arg.typeName); + const name = this.getEntityNameRoot(generic_arg.typeName); const interface_node = this.interface_map.get(name); if (interface_node) { this.props_interface.name = name; @@ -394,13 +428,13 @@ export class HoistableInterfaces { ) { const walk = (node: ts.Node) => { if (ts.isTypeReferenceNode(node)) { - const type_name = this.getEntityNameText(node.typeName); + const type_name = this.getEntityNameRoot(node.typeName); if (!generics.includes(type_name)) { type_dependencies.add(type_name); } } else if (ts.isTypeQueryNode(node)) { // Handle 'typeof' expressions: e.g., foo: typeof bar - value_dependencies.add(this.getEntityNameText(node.exprName)); + value_dependencies.add(this.getEntityNameRoot(node.exprName)); } ts.forEachChild(node, walk); @@ -410,15 +444,16 @@ export class HoistableInterfaces { } /** - * Retrieves the full text of an EntityName (handles nested names). + * Retrieves the top-level variable/namespace of an EntityName (handles nested names). + * ex: `foo.bar.baz` -> `foo` * @param entity_name The EntityName to extract text from. - * @returns The full name as a string. + * @returns The top-level name as a string. */ - private getEntityNameText(entity_name: ts.EntityName): string { + private getEntityNameRoot(entity_name: ts.EntityName): string { if (ts.isIdentifier(entity_name)) { return entity_name.text; } else { - return this.getEntityNameText(entity_name.left) + '.' + entity_name.right.text; + return this.getEntityNameRoot(entity_name.left); } } } diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts new file mode 100644 index 000000000..f063d70b8 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/expectedv2.ts @@ -0,0 +1,19 @@ +/// +;; + interface A { + type: string; + };; + + interface Props extends A { + a: string; + };function $$render() { + + + + const { }: Props = $props(); +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte new file mode 100644 index 000000000..dec767819 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-6.v5/input.svelte @@ -0,0 +1,10 @@ + diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts new file mode 100644 index 000000000..22f05c3f8 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/expectedv2.ts @@ -0,0 +1,17 @@ +/// +;; +type Props = { + data: {cfg: string}; +};;function $$render() { + + +let { data }: Props = $props(); + +type A = typeof data.cfg; +type B = (typeof data)['cfg']; +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte new file mode 100644 index 000000000..42e0fbb2a --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-10.v5/input.svelte @@ -0,0 +1,9 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts new file mode 100644 index 000000000..ed54ba6dc --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/expectedv2.ts @@ -0,0 +1,20 @@ +/// +;function $$render() { + + const a = 1; + +interface A { + Abc: typeof a +} + +interface Abc { + foo: A.Abc +} + +let {}: Abc = $props(); +; +async () => {}; +return { props: {} as any as Abc, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte new file mode 100644 index 000000000..c2532ebe6 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-11.v5/input.svelte @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts new file mode 100644 index 000000000..1bcbfe8c5 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/expectedv2.ts @@ -0,0 +1,20 @@ +/// +;function $$render() { + +const a = 1; + +namespace A { + export type Abc = typeof a +} + +interface Abc { + foo: A.Abc +} + +let {}: Abc = $props(); +; +async () => {}; +return { props: {} as any as Abc, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte new file mode 100644 index 000000000..897e08c65 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-12.v5/input.svelte @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts new file mode 100644 index 000000000..1525d0cc2 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/expectedv2.ts @@ -0,0 +1,20 @@ +/// +; + namespace A { + export type Abd = number + } +;;function $$render() { + +interface A { + Abc: number +} + +let {Abc}: A = $props() +; +async () => { + +}; +return { props: {} as any as A, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte new file mode 100644 index 000000000..1617746f0 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-13.v5/input.svelte @@ -0,0 +1,13 @@ + + + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts new file mode 100644 index 000000000..27b661d11 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/expectedv2.ts @@ -0,0 +1,19 @@ +/// +; + enum A { + } +;;function $$render() { + +interface A { + Abc: number +} + +let {Abc}: A = $props() +; +async () => { + +}; +return { props: {} as any as A, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); +type Input__SvelteComponent_ = ReturnType; +export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte new file mode 100644 index 000000000..6d555bb69 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-14.v5/input.svelte @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts new file mode 100644 index 000000000..60855f1fb --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/expectedv2.ts @@ -0,0 +1,36 @@ +/// +;function $$render() { + + interface WithItems { + items: T[]; + } + + interface Props extends WithItems { + prop: T; + }; + let { prop }: Props = $props(); +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +class __sveltets_Render { + props() { + return $$render().props; + } + events() { + return $$render().events; + } + slots() { + return $$render().slots; + } + bindings() { return __sveltets_$$bindings(''); } + exports() { return {}; } +} + +interface $$IsomorphicComponent { + new (options: import('svelte').ComponentConstructorOptions['props']>>): import('svelte').SvelteComponent['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> & { $$bindings?: ReturnType<__sveltets_Render['bindings']> } & ReturnType<__sveltets_Render['exports']>; + (internal: unknown, props: ReturnType<__sveltets_Render['props']> & {}): ReturnType<__sveltets_Render['exports']>; + z_$$bindings?: ReturnType<__sveltets_Render['bindings']>; +} +const Input__SvelteComponent_: $$IsomorphicComponent = null as any; +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType>; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte new file mode 100644 index 000000000..11461621f --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-8.v5/input.svelte @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts new file mode 100644 index 000000000..39b62f5b3 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/expectedv2.ts @@ -0,0 +1,31 @@ +/// +;function $$render() { + + interface Props extends T { + }; + let { a }: Props = $props(); +; +async () => {}; +return { props: {} as any as Props, exports: {}, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +class __sveltets_Render { + props() { + return $$render().props; + } + events() { + return $$render().events; + } + slots() { + return $$render().slots; + } + bindings() { return __sveltets_$$bindings(''); } + exports() { return {}; } +} + +interface $$IsomorphicComponent { + new (options: import('svelte').ComponentConstructorOptions['props']>>): import('svelte').SvelteComponent['props']>, ReturnType<__sveltets_Render['events']>, ReturnType<__sveltets_Render['slots']>> & { $$bindings?: ReturnType<__sveltets_Render['bindings']> } & ReturnType<__sveltets_Render['exports']>; + (internal: unknown, props: ReturnType<__sveltets_Render['props']> & {}): ReturnType<__sveltets_Render['exports']>; + z_$$bindings?: ReturnType<__sveltets_Render['bindings']>; +} +const Input__SvelteComponent_: $$IsomorphicComponent = null as any; +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType>; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte new file mode 100644 index 000000000..fd8a6f6f1 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-runes-hoistable-props-false-9.v5/input.svelte @@ -0,0 +1,5 @@ + \ No newline at end of file From ddc62b874be822111ce424a9d2ba64f752eaeab4 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 25 Apr 2025 15:39:01 +0800 Subject: [PATCH 2/7] fix: ensure typed exports are marked as used (#2746) #2717 --- packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts | 4 ++-- .../svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts | 2 +- .../expectedv2.ts | 2 +- .../ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts index 2703f6a80..c7fcb2879 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/ExportedNames.ts @@ -791,11 +791,11 @@ export class ExportedNames { private createReturnElements( names: Array<[string, ExportedName]>, dontAddTypeDef: boolean, - omitTyped = false + onlyTyped = false ): string[] { return names .map(([key, value]) => { - if (omitTyped && value.type) return; + if (onlyTyped && !value.type) return; // Important to not use shorthand props for rename functionality return `${dontAddTypeDef && value.doc ? `\n${value.doc}` : ''}${ value.identifierText || key diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts index d05d48969..23428b25c 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-export-list-runes.v5/expectedv2.ts @@ -20,7 +20,7 @@ ; async () => { { svelteHTML.createElement("svelte:options", {"runes":true,});} }; -return { props: {} as Record, exports: {Foo: Foo,bar: bar,RenamedFoo: RenameFoo,renamedbar: renamebar} as any as { name1: string,name2: string,name3: string,name4: string,renamed1: string,renamed2: string,Foo: typeof Foo,bar: typeof bar,baz: string,RenamedFoo: typeof RenameFoo,renamedbar: typeof renamebar,renamedbaz: string }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as Record, exports: {name1: name1,name2: name2,name3: name3,name4: name4,renamed1: rename1,renamed2: rename2,baz: baz,renamedbaz: renamebaz} as any as { name1: string,name2: string,name3: string,name4: string,renamed1: string,renamed2: string,Foo: typeof Foo,bar: typeof bar,baz: string,RenamedFoo: typeof RenameFoo,renamedbar: typeof renamebar,renamedbaz: string }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} const Input__SvelteComponent_ = __sveltets_2_fn_component($$render()); type Input__SvelteComponent_ = ReturnType; export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts index c788dc05e..b2051ddd0 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune-unchanged.v5/expectedv2.ts @@ -6,7 +6,7 @@ let { form, data }:/*Ωignore_startΩ*/$$ComponentProps/*Ωignore_endΩ*/ = $props(); ; async () => {}; -return { props: {} as any as $$ComponentProps, exports: {} as any as { snapshot: any }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as any as $$ComponentProps, exports: {snapshot: snapshot} as any as { snapshot: any }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} const Page__SvelteComponent_ = __sveltets_2_fn_component($$render()); type Page__SvelteComponent_ = ReturnType; export default Page__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts index 4a33a614f..ce2a45ef9 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/ts-sveltekit-autotypes-$props-rune.v5/expectedv2.ts @@ -5,7 +5,7 @@ let { form, data }: $$ComponentProps = $props(); ; async () => {}; -return { props: {} as any as $$ComponentProps, exports: {snapshot: snapshot} as any as { snapshot: typeof snapshot }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} +return { props: {} as any as $$ComponentProps, exports: {} as any as { snapshot: typeof snapshot }, bindings: __sveltets_$$bindings(''), slots: {}, events: {} }} const Page__SvelteComponent_ = __sveltets_2_fn_component($$render()); type Page__SvelteComponent_ = ReturnType; export default Page__SvelteComponent_; \ No newline at end of file From 5366e7899cb44d73c930d32ffd7d9c29273507ca Mon Sep 17 00:00:00 2001 From: datstarkey <35140564+datstarkey@users.noreply.github.com> Date: Fri, 2 May 2025 15:19:39 +0100 Subject: [PATCH 3/7] feat: support "add missing imports on save" (#2744) Fixes #2616 Adds the source action to addMissingImports, reusing the same quick fix action as "import all missing" --- .../features/CodeActionsProvider.ts | 60 +++++++++++-- packages/language-server/src/server.ts | 6 +- .../features/CodeActionsProvider.test.ts | 87 ++++++++++++++++++- ...odeaction-custom-fix-all-component5.svelte | 7 ++ ...odeaction-custom-fix-all-component6.svelte | 10 +++ 5 files changed, 161 insertions(+), 9 deletions(-) create mode 100644 packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte create mode 100644 packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 581a46d8d..6f6bd8970 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -1,3 +1,4 @@ +import { internalHelpers } from 'svelte2tsx'; import ts from 'typescript'; import { CancellationToken, @@ -24,6 +25,7 @@ import { } from '../../../lib/documents'; import { LSConfigManager } from '../../../ls-config'; import { + createGetCanonicalFileName, flatten, getIndent, isNotNullOrUndefined, @@ -37,6 +39,7 @@ import { import { CodeActionsProvider } from '../../interfaces'; import { DocumentSnapshot, SvelteDocumentSnapshot } from '../DocumentSnapshot'; import { LSAndTSDocResolver } from '../LSAndTSDocResolver'; +import { LanguageServiceContainer } from '../service'; import { changeSvelteComponentName, convertRange, @@ -44,6 +47,7 @@ import { toGeneratedSvelteComponentName } from '../utils'; import { CompletionsProviderImpl } from './CompletionProvider'; +import { DiagnosticCode } from './DiagnosticsProvider'; import { findClosestContainingNode, FormatCodeBasis, @@ -53,15 +57,12 @@ import { isTextSpanInGeneratedCode, SnapshotMap } from './utils'; -import { DiagnosticCode } from './DiagnosticsProvider'; -import { createGetCanonicalFileName } from '../../../utils'; -import { LanguageServiceContainer } from '../service'; -import { internalHelpers } from 'svelte2tsx'; /** * TODO change this to protocol constant if it's part of the protocol */ export const SORT_IMPORT_CODE_ACTION_KIND = 'source.sortImports'; +export const ADD_MISSING_IMPORTS_CODE_ACTION_KIND = 'source.addMissingImports'; interface RefactorArgs { type: 'refactor'; @@ -121,6 +122,10 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { ); } + if (context.only?.[0] === ADD_MISSING_IMPORTS_CODE_ACTION_KIND) { + return await this.addMissingImports(document, cancellationToken); + } + // for source action command (all source.xxx) // vscode would show different source code action kinds to choose from if (context.only?.[0] === CodeActionKind.Source) { @@ -130,7 +135,8 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { document, cancellationToken, /**skipDestructiveCodeActions */ true - )) + )), + ...(await this.addMissingImports(document, cancellationToken)) ]; } @@ -1553,4 +1559,48 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { private async getLSAndTSDoc(document: Document) { return this.lsAndTsDocResolver.getLSAndTSDoc(document); } + + private async addMissingImports( + document: Document, + cancellationToken?: CancellationToken + ): Promise { + // Re-introduce LS/TSDoc resolution and diagnostic check + const { lang, tsDoc } = await this.getLSAndTSDoc(document); + if (cancellationToken?.isCancellationRequested) { + return []; + } + + // Check if there are any relevant "cannot find name" diagnostics + const diagnostics = lang.getSemanticDiagnostics(tsDoc.filePath); + const hasMissingImports = diagnostics.some( + (diag) => + (diag.code === DiagnosticCode.CANNOT_FIND_NAME || + diag.code === DiagnosticCode.CANNOT_FIND_NAME_X_DID_YOU_MEAN_Y) && + // Ensure the diagnostic is not in generated code + !isTextSpanInGeneratedCode(tsDoc.getFullText(), { + start: diag.start ?? 0, + length: diag.length ?? 0 + }) + ); + + // Only return the action if there are potential imports to add + if (!hasMissingImports) { + return []; + } + + // If imports might be needed, create the deferred action + const codeAction = CodeAction.create( + FIX_IMPORT_FIX_DESCRIPTION, + ADD_MISSING_IMPORTS_CODE_ACTION_KIND + ); + + const data: QuickFixAllResolveInfo = { + uri: document.uri, + fixName: FIX_IMPORT_FIX_NAME, + fixId: FIX_IMPORT_FIX_ID + }; + codeAction.data = data; + + return [codeAction]; + } } diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index f1aa52ca0..af5f92a60 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -45,7 +45,10 @@ import { debounceThrottle, isNotNullOrUndefined, normalizeUri, urlToPath } from import { FallbackWatcher } from './lib/FallbackWatcher'; import { configLoader } from './lib/documents/configLoader'; import { setIsTrusted } from './importPackage'; -import { SORT_IMPORT_CODE_ACTION_KIND } from './plugins/typescript/features/CodeActionsProvider'; +import { + SORT_IMPORT_CODE_ACTION_KIND, + ADD_MISSING_IMPORTS_CODE_ACTION_KIND +} from './plugins/typescript/features/CodeActionsProvider'; import { createLanguageServices } from './plugins/css/service'; import { FileSystemProvider } from './plugins/css/FileSystemProvider'; @@ -270,6 +273,7 @@ export function startServer(options?: LSOptions) { CodeActionKind.QuickFix, CodeActionKind.SourceOrganizeImports, SORT_IMPORT_CODE_ACTION_KIND, + ADD_MISSING_IMPORTS_CODE_ACTION_KIND, ...(clientSupportApplyEditCommand ? [CodeActionKind.Refactor] : []) ].filter( clientSupportedCodeActionKinds && diff --git a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts index e622ae1c2..a07288542 100644 --- a/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CodeActionsProvider.test.ts @@ -1,5 +1,7 @@ import * as assert from 'assert'; import * as path from 'path'; +import { VERSION } from 'svelte/compiler'; +import { internalHelpers } from 'svelte2tsx'; import ts from 'typescript'; import { CancellationTokenSource, @@ -12,17 +14,16 @@ import { import { Document, DocumentManager } from '../../../../src/lib/documents'; import { LSConfigManager } from '../../../../src/ls-config'; import { + ADD_MISSING_IMPORTS_CODE_ACTION_KIND, CodeActionsProviderImpl, SORT_IMPORT_CODE_ACTION_KIND } from '../../../../src/plugins/typescript/features/CodeActionsProvider'; import { CompletionsProviderImpl } from '../../../../src/plugins/typescript/features/CompletionProvider'; +import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider'; import { LSAndTSDocResolver } from '../../../../src/plugins/typescript/LSAndTSDocResolver'; import { __resetCache } from '../../../../src/plugins/typescript/service'; import { pathToUrl } from '../../../../src/utils'; import { recursiveServiceWarmup } from '../test-utils'; -import { DiagnosticCode } from '../../../../src/plugins/typescript/features/DiagnosticsProvider'; -import { VERSION } from 'svelte/compiler'; -import { internalHelpers } from 'svelte2tsx'; const testDir = path.join(__dirname, '..'); const indent = ' '.repeat(4); @@ -2229,4 +2230,84 @@ describe('CodeActionsProvider', function () { after(() => { __resetCache(); }); + + it('provides source action for adding all missing imports', async () => { + const { provider, document } = setup('codeaction-custom-fix-all-component5.svelte'); + + const range = Range.create(Position.create(4, 1), Position.create(4, 15)); + + // Request the specific source action + const codeActions = await provider.getCodeActions(document, range, { + diagnostics: [], // Diagnostics might not be needed here if we only want the source action by kind + only: [ADD_MISSING_IMPORTS_CODE_ACTION_KIND] + }); + + assert.ok(codeActions.length > 0, 'No code actions found'); + + // Find the action by its kind + const addImportsAction = codeActions.find((action) => action.data); + + // Ensure the action was found and has data (as it's now deferred) + assert.ok(addImportsAction, 'Add missing imports action should be found'); + assert.ok( + addImportsAction.data, + 'Add missing imports action should have data for resolution' + ); + + // Resolve the action to get the edits + const resolvedAction = await provider.resolveCodeAction(document, addImportsAction); + + // Assert the edits on the resolved action + assert.ok(resolvedAction.edit, 'Resolved action should have an edit'); + (resolvedAction.edit?.documentChanges?.[0])?.edits.forEach( + (edit) => (edit.newText = harmonizeNewLines(edit.newText)) + ); + + assert.deepStrictEqual(resolvedAction.edit, { + documentChanges: [ + { + edits: [ + { + newText: + `\n${indent}import FixAllImported from \"./importing/FixAllImported.svelte\";\n` + + `${indent}import FixAllImported2 from \"./importing/FixAllImported2.svelte\";\n`, + range: { + start: { + character: 18, + line: 0 + }, + end: { + character: 18, + line: 0 + } + } + } + ], + textDocument: { + uri: getUri('codeaction-custom-fix-all-component5.svelte'), + version: null + } + } + ] + }); + + // Optional: Verify the kind and title remain correct on the resolved action + assert.strictEqual(resolvedAction.kind, ADD_MISSING_IMPORTS_CODE_ACTION_KIND); + assert.strictEqual(resolvedAction.title, 'Add all missing imports'); + }); + + it('provides source action for adding all missing imports only when imports are missing', async () => { + const { provider, document } = setup('codeaction-custom-fix-all-component6.svelte'); + + const codeActions = await provider.getCodeActions( + document, + Range.create(Position.create(1, 4), Position.create(1, 5)), + { + diagnostics: [], // No diagnostics = no missing imports + only: [ADD_MISSING_IMPORTS_CODE_ACTION_KIND] + } + ); + + assert.deepStrictEqual(codeActions, []); + }); }); diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte new file mode 100644 index 000000000..15f6d8fd7 --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component5.svelte @@ -0,0 +1,7 @@ + + + + + diff --git a/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte new file mode 100644 index 000000000..6c32ebc5e --- /dev/null +++ b/packages/language-server/test/plugins/typescript/testfiles/code-actions/codeaction-custom-fix-all-component6.svelte @@ -0,0 +1,10 @@ + + + + + From b887cbe2fc1882dbbbe8bd9bbd12141443bf2446 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 2 May 2025 22:20:11 +0800 Subject: [PATCH 4/7] chore: bump vscode-html/css-language-service (#2752) * chore: bump vscode-html/css-language-service * update test --- packages/language-server/package.json | 4 +- .../test/plugins/css/CSSPlugin.test.ts | 39 +++++++++++++++++-- .../test/plugins/html/HTMLPlugin.test.ts | 5 ++- pnpm-lock.yaml | 34 ++++++++-------- 4 files changed, 59 insertions(+), 23 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 7933acf96..eafda89b6 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -64,8 +64,8 @@ "svelte2tsx": "workspace:~", "typescript": "^5.8.2", "typescript-auto-import-cache": "^0.3.5", - "vscode-css-languageservice": "~6.3.2", - "vscode-html-languageservice": "~5.3.2", + "vscode-css-languageservice": "~6.3.5", + "vscode-html-languageservice": "~5.4.0", "vscode-languageserver": "9.0.1", "vscode-languageserver-protocol": "3.17.5", "vscode-languageserver-types": "3.17.5", diff --git a/packages/language-server/test/plugins/css/CSSPlugin.test.ts b/packages/language-server/test/plugins/css/CSSPlugin.test.ts index 5e9c4bf32..9a82c53ff 100644 --- a/packages/language-server/test/plugins/css/CSSPlugin.test.ts +++ b/packages/language-server/test/plugins/css/CSSPlugin.test.ts @@ -72,8 +72,8 @@ describe('CSS Plugin', () => { kind: 'markdown', value: "Specifies the height of the content area, padding area or border area \\(depending on 'box\\-sizing'\\) of certain boxes\\.\n\n" + - '(Edge 12, Firefox 1, Safari 1, Chrome 1, IE 4, Opera 7)\n\n' + - 'Syntax: auto | <length> | <percentage> | min\\-content | max\\-content | fit\\-content | fit\\-content\\(<length\\-percentage>\\)\n\n' + + '![Baseline icon](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+) _Widely available across major browsers (Baseline since 2015)_\n\n' + + 'Syntax: auto | <length\\-percentage \\[0,∞\\]> | min\\-content | max\\-content | fit\\-content | fit\\-content\\(<length\\-percentage \\[0,∞\\]>\\) | <calc\\-size\\(\\)> | <anchor\\-size\\(\\)>\n\n' + '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/height)' }, range: Range.create(0, 12, 0, 24) @@ -105,7 +105,8 @@ describe('CSS Plugin', () => { documentation: { kind: 'markdown', value: - 'Defines character set of the document\\.\n\n(Edge 12, Firefox 1, Safari 4, Chrome 2, IE 5, Opera 9)\n\n' + + 'Defines character set of the document\\.\n\n' + + '![Baseline icon](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+) _Widely available across major browsers (Baseline since 2015)_\n\n' + '[MDN Reference](https://developer.mozilla.org/docs/Web/CSS/@charset)' }, textEdit: TextEdit.insert(Position.create(0, 7), '@charset'), @@ -344,6 +345,38 @@ describe('CSS Plugin', () => { }, newText: 'hwb(240 0% -25400%)' } + }, + { + label: 'lab(3880.51% 6388.69 -8701.22)', + textEdit: { + newText: 'lab(3880.51% 6388.69 -8701.22)', + range: { + end: { + character: 21, + line: 0 + }, + start: { + character: 17, + line: 0 + } + } + } + }, + { + label: 'lch(3880.51% 10794.75 306.29)', + textEdit: { + newText: 'lch(3880.51% 10794.75 306.29)', + range: { + end: { + character: 21, + line: 0 + }, + start: { + character: 17, + line: 0 + } + } + } } ]); }); diff --git a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts index 4a45b6310..eff038a11 100644 --- a/packages/language-server/test/plugins/html/HTMLPlugin.test.ts +++ b/packages/language-server/test/plugins/html/HTMLPlugin.test.ts @@ -35,7 +35,10 @@ describe('HTML Plugin', () => { assert.deepStrictEqual(plugin.doHover(document, Position.create(0, 2)), { contents: { kind: 'markdown', - value: 'The h1 element represents a section heading.\n\n[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Element/Heading_Elements)' + value: + 'The h1 element represents a section heading.\n\n' + + '![Baseline icon](data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTgiIGhlaWdodD0iMTAiIHZpZXdCb3g9IjAgMCA1NDAgMzAwIiBmaWxsPSJub25lIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPgogIDxzdHlsZT4KICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgIGZpbGw6ICNDNEVFRDA7IC8qIExpZ2h0IG1vZGUgKi8KICAgIH0KCiAgICBAbWVkaWEgKHByZWZlcnMtY29sb3Itc2NoZW1lOiBkYXJrKSB7CiAgICAgIC5ncmVlbi1zaGFwZSB7CiAgICAgICAgZmlsbDogIzEyNTIyNTsgLyogRGFyayBtb2RlICovCiAgICAgIH0KICAgIH0KICA8L3N0eWxlPgogIDxwYXRoIGQ9Ik00MjAgMzBMMzkwIDYwTDQ4MCAxNTBMMzkwIDI0MEwzMzAgMTgwTDMwMCAyMTBMMzkwIDMwMEw1NDAgMTUwTDQyMCAzMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0xNTAgMEwzMCAxMjBMNjAgMTUwTDE1MCA2MEwyMTAgMTIwTDI0MCA5MEwxNTAgMFoiIGNsYXNzPSJncmVlbi1zaGFwZSIvPgogIDxwYXRoIGQ9Ik0zOTAgMEw0MjAgMzBMMTUwIDMwMEwwIDE1MEwzMCAxMjBMMTUwIDI0MEwzOTAgMFoiIGZpbGw9IiMxRUE0NDYiLz4KPC9zdmc+) _Widely available across major browsers (Baseline since 2015)_\n\n' + + '[MDN Reference](https://developer.mozilla.org/docs/Web/HTML/Reference/Elements/Heading_Elements)' }, range: Range.create(0, 1, 0, 3) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 905264a94..26e32643a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,11 +64,11 @@ importers: specifier: ^0.3.5 version: 0.3.5 vscode-css-languageservice: - specifier: ~6.3.2 - version: 6.3.2 + specifier: ~6.3.5 + version: 6.3.5 vscode-html-languageservice: - specifier: ~5.3.2 - version: 5.3.2 + specifier: ~5.4.0 + version: 5.4.0 vscode-languageserver: specifier: 9.0.1 version: 9.0.1 @@ -80,7 +80,7 @@ importers: version: 3.17.5 vscode-uri: specifier: ~3.0.0 - version: 3.0.8 + version: 3.1.0 devDependencies: '@types/estree': specifier: ^0.0.42 @@ -181,7 +181,7 @@ importers: version: 3.17.2 vscode-uri: specifier: ~3.0.0 - version: 3.0.8 + version: 3.1.0 packages/svelte-vscode: dependencies: @@ -1302,11 +1302,11 @@ packages: vfile-message@3.1.4: resolution: {integrity: sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==} - vscode-css-languageservice@6.3.2: - resolution: {integrity: sha512-GEpPxrUTAeXWdZWHev1OJU9lz2Q2/PPBxQ2TIRmLGvQiH3WZbqaNoute0n0ewxlgtjzTW3AKZT+NHySk5Rf4Eg==} + vscode-css-languageservice@6.3.5: + resolution: {integrity: sha512-ehEIMXYPYEz/5Svi2raL9OKLpBt5dSAdoCFoLpo0TVFKrVpDemyuQwS3c3D552z/qQCg3pMp8oOLMObY6M3ajQ==} - vscode-html-languageservice@5.3.2: - resolution: {integrity: sha512-3MgFQqVG+iQVNG7QI/slaoL7lJpne0nssX082kjUF1yn/YJa8BWCLeCJjM0YpTlp8A7JT1+J22mk4qSPx3NjSQ==} + vscode-html-languageservice@5.4.0: + resolution: {integrity: sha512-9/cbc90BSYCghmHI7/VbWettHZdC7WYpz2g5gBK6UDUI1MkZbM773Q12uAYJx9jzAiNHPpyo6KzcwmcnugncAQ==} vscode-jsonrpc@8.0.2: resolution: {integrity: sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==} @@ -1359,8 +1359,8 @@ packages: vscode-uri@2.1.2: resolution: {integrity: sha512-8TEXQxlldWAuIODdukIb+TR5s+9Ds40eSJrw+1iDDA9IFORPjMELarNQE3myz5XIkWWpdprmJjm1/SxMlWOC8A==} - vscode-uri@3.0.8: - resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + vscode-uri@3.1.0: + resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==} which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} @@ -2372,19 +2372,19 @@ snapshots: '@types/unist': 2.0.6 unist-util-stringify-position: 3.0.3 - vscode-css-languageservice@6.3.2: + vscode-css-languageservice@6.3.5: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 - vscode-html-languageservice@5.3.2: + vscode-html-languageservice@5.4.0: dependencies: '@vscode/l10n': 0.0.18 vscode-languageserver-textdocument: 1.0.12 vscode-languageserver-types: 3.17.5 - vscode-uri: 3.0.8 + vscode-uri: 3.1.0 vscode-jsonrpc@8.0.2: {} @@ -2437,7 +2437,7 @@ snapshots: vscode-uri@2.1.2: {} - vscode-uri@3.0.8: {} + vscode-uri@3.1.0: {} which@2.0.2: dependencies: From 0c341bd264610f0becf53e29158444890903bd25 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 2 May 2025 22:24:39 +0800 Subject: [PATCH 5/7] fix: Move hoisted snippet to right after import (#2753) #2653 --- packages/svelte2tsx/src/svelte2tsx/index.ts | 23 +++++++++++++------ .../nodes/handleImportDeclaration.ts | 4 ++-- .../svelte2tsx/src/svelte2tsx/utils/tsAst.ts | 4 ++++ .../snippet-module-hoist-3.v5/expectedv2.ts | 7 +++--- .../snippet-module-hoist-5.v5/expectedv2.ts | 12 ++++++++++ .../snippet-module-hoist-5.v5/input.svelte | 5 ++++ .../snippet-module-hoist-6.v5/expectedv2.ts | 12 ++++++++++ .../snippet-module-hoist-6.v5/input.svelte | 5 ++++ 8 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts create mode 100644 packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts index a3b45221f..4b2ad2913 100644 --- a/packages/svelte2tsx/src/svelte2tsx/index.ts +++ b/packages/svelte2tsx/src/svelte2tsx/index.ts @@ -10,6 +10,7 @@ import { processInstanceScriptContent } from './processInstanceScriptContent'; import { createModuleAst, ModuleAst, processModuleScriptTag } from './processModuleScriptTag'; import path from 'path'; import { parse, VERSION } from 'svelte/compiler'; +import { getTopLevelImports } from './utils/tsAst'; function processSvelteTemplate( str: MagicString, @@ -170,6 +171,20 @@ export function svelte2tsx( } if (moduleScriptTag || scriptTag) { + let snippetHoistTargetForModule = 0; + if (rootSnippets.length) { + if (scriptTag) { + snippetHoistTargetForModule = scriptTag.start + 1; // +1 because imports are also moved at that position, and we want to move interfaces after imports + } else { + const imports = getTopLevelImports(moduleAst.tsAst); + const lastImport = imports[imports.length - 1]; + snippetHoistTargetForModule = lastImport + ? lastImport.end + moduleAst.astOffset + : moduleAst.astOffset; + str.appendLeft(snippetHoistTargetForModule, '\n'); + } + } + for (const [start, end, globals] of rootSnippets) { const hoist_to_module = moduleScriptTag && @@ -179,13 +194,7 @@ export function svelte2tsx( )); if (hoist_to_module) { - str.move( - start, - end, - scriptTag - ? scriptTag.start + 1 // +1 because imports are also moved at that position, and we want to move interfaces after imports - : instanceScriptTarget - ); + str.move(start, end, snippetHoistTargetForModule); } else if (scriptTag) { str.move(start, end, renderFunctionStart); } diff --git a/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts b/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts index 4f022260b..9daeb78e5 100644 --- a/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts +++ b/packages/svelte2tsx/src/svelte2tsx/nodes/handleImportDeclaration.ts @@ -1,6 +1,6 @@ import MagicString from 'magic-string'; import ts from 'typescript'; -import { moveNode } from '../utils/tsAst'; +import { getTopLevelImports, moveNode } from '../utils/tsAst'; /** * move imports to top of script so they appear outside our render function @@ -25,7 +25,7 @@ export function handleFirstInstanceImport( hasModuleScript: boolean, str: MagicString ) { - const imports = tsAst.statements.filter(ts.isImportDeclaration).sort((a, b) => a.end - b.end); + const imports = getTopLevelImports(tsAst); const firstImport = imports[0]; if (!firstImport) { return; diff --git a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts index a189c75b5..d8c11ac64 100644 --- a/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts +++ b/packages/svelte2tsx/src/svelte2tsx/utils/tsAst.ts @@ -273,3 +273,7 @@ function isNewGroup(sourceFile: ts.SourceFile, topLevelImportDecl: ts.Node, scan return false; } + +export function getTopLevelImports(sourceFile: ts.SourceFile): ts.ImportDeclaration[] { + return sourceFile.statements.filter(ts.isImportDeclaration).sort((a, b) => a.end - b.end); +} diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts index 67a5d3f9d..c540fe409 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-3.v5/expectedv2.ts @@ -1,11 +1,12 @@ /// ; - let foo = true; -; const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { + const hoistable1/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { { svelteHTML.createElement("div", {}); } };return __sveltets_2_any(0)}; const hoistable2/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => { { svelteHTML.createElement("div", {});foo; } -};return __sveltets_2_any(0)};;function $$render() { +};return __sveltets_2_any(0)}; + let foo = true; +;;function $$render() { async () => { diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts new file mode 100644 index 000000000..1063eae01 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/expectedv2.ts @@ -0,0 +1,12 @@ +/// +; + import {} from 'svelte' + const foo/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {};return __sveltets_2_any(0)}; +;;function $$render() { +async () => { + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event($$render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte new file mode 100644 index 000000000..6321ca3bf --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-5.v5/input.svelte @@ -0,0 +1,5 @@ + + +{#snippet foo()}{/snippet} \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts new file mode 100644 index 000000000..39bb66302 --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/expectedv2.ts @@ -0,0 +1,12 @@ +/// +; + const _foo/*Ωignore_positionΩ*/ = ()/*Ωignore_startΩ*/: ReturnType/*Ωignore_endΩ*/ => { async ()/*Ωignore_positionΩ*/ => {};return __sveltets_2_any(0)}; + export const foo = _foo; +;;function $$render() { +async () => { + +}; +return { props: /** @type {Record} */ ({}), exports: {}, bindings: "", slots: {}, events: {} }} +const Input__SvelteComponent_ = __sveltets_2_isomorphic_component(__sveltets_2_partial(__sveltets_2_with_any_event($$render()))); +/*Ωignore_startΩ*/type Input__SvelteComponent_ = InstanceType; +/*Ωignore_endΩ*/export default Input__SvelteComponent_; \ No newline at end of file diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte new file mode 100644 index 000000000..8a2e6f5ed --- /dev/null +++ b/packages/svelte2tsx/test/svelte2tsx/samples/snippet-module-hoist-6.v5/input.svelte @@ -0,0 +1,5 @@ + + +{#snippet _foo()}{/snippet} \ No newline at end of file From 0c4e103b7e4212afd10dfc36766a0f8adb28be35 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Fri, 2 May 2025 22:29:54 +0800 Subject: [PATCH 6/7] fix: prevent error with unclosed tag followed by LF or end of file (#2750) #2742 The reason that it happens to LF but not CRLF is that CRLF has two characters, so it doesn't touch the range of the start tag name, and therefore won't trigger the magic string error. This should also improve auto-import performance a bit because TypeScript only needs to compute auto-imports for Button and not a global auto-import. --- .../features/CodeActionsProvider.ts | 2 +- .../typescript/features/CompletionProvider.ts | 13 ++++++++++-- .../src/plugins/typescript/features/utils.ts | 4 ++-- .../features/CompletionProvider.test.ts | 1 + .../src/htmlxtojsx_v2/nodes/Element.ts | 19 +++++++++++++++--- .../htmlxtojsx_v2/nodes/InlineComponent.ts | 17 +++++++++++++--- .../expected.error.json | 20 +++++++++++++++++++ .../expectedv2.js | 5 +++++ .../input.svelte | 9 +++++++++ .../expected.error.json | 20 +++++++++++++++++++ .../expectedv2.js | 5 +++++ .../input.svelte | 9 +++++++++ 12 files changed, 113 insertions(+), 11 deletions(-) create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-component-no-attr.v5/expected.error.json create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-component-no-attr.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-component-no-attr.v5/input.svelte create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-element-no-attr.v5/expected.error.json create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-element-no-attr.v5/expectedv2.js create mode 100644 packages/svelte2tsx/test/htmlx2jsx/samples/editing-unclosed-element-no-attr.v5/input.svelte diff --git a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts index 6f6bd8970..244ae4e12 100644 --- a/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CodeActionsProvider.ts @@ -383,7 +383,7 @@ export class CodeActionsProviderImpl implements CodeActionsProvider { if (editForThisFile?.edits.length) { const [first] = editForThisFile.edits; first.newText = - getNewScriptStartTag(this.configManager.getConfig()) + + getNewScriptStartTag(this.configManager.getConfig(), formatCodeBasis.newLine) + formatCodeBasis.baseIndent + first.newText.trimStart(); diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 48b6db53f..bc1764d87 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -595,7 +595,8 @@ export class CompletionsProviderImpl implements CompletionsProvider ({ label: name, kind: CompletionItemKind.Property, - textEdit: TextEdit.replace(this.cloneRange(replacementRange), name) + textEdit: TextEdit.replace(this.cloneRange(replacementRange), name), + commitCharacters: [] })); } @@ -1131,9 +1132,17 @@ export class CompletionsProviderImpl implements CompletionsProvider${newLine}` + `${getNewScriptStartTag(config, newLine)}${newText}${newLine}` ); } diff --git a/packages/language-server/src/plugins/typescript/features/utils.ts b/packages/language-server/src/plugins/typescript/features/utils.ts index a8b0229e3..954ef03bc 100644 --- a/packages/language-server/src/plugins/typescript/features/utils.ts +++ b/packages/language-server/src/plugins/typescript/features/utils.ts @@ -429,10 +429,10 @@ export function findChildOfKind(node: ts.Node, kind: ts.SyntaxKind): ts.Node | u } } -export function getNewScriptStartTag(lsConfig: Readonly) { +export function getNewScriptStartTag(lsConfig: Readonly, newLine: string) { const lang = lsConfig.svelte.defaultScriptLanguage; const scriptLang = lang === 'none' ? '' : ` lang="${lang}"`; - return `${ts.sys.newLine}`; + return `${newLine}`; } export function checkRangeMappingWithGeneratedSemi( diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index e06fe7278..f0f3ea933 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -298,6 +298,7 @@ describe('CompletionProviderImpl', function () { assert.deepStrictEqual(item, { label: 'custom-element', kind: CompletionItemKind.Property, + commitCharacters: [], textEdit: { range: { start: { line: 0, character: 1 }, end: { line: 0, character: 2 } }, newText: 'custom-element' diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts index b3ef9233c..bd8947c64 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts @@ -43,6 +43,7 @@ export class Element { private isSelfclosing: boolean; public tagName: string; public child?: any; + private tagNameEnd: number; // Add const $$xxx = ... only if the variable name is actually used // in order to prevent "$$xxx is defined but never used" TS hints @@ -74,7 +75,7 @@ export class Element { this.startTagStart = this.node.start; this.startTagEnd = this.computeStartTagEnd(); - const tagEnd = this.startTagStart + this.node.name.length + 1; + const tagEnd = (this.tagNameEnd = this.startTagStart + this.node.name.length + 1); // Ensure deleted characters are mapped to the attributes object so we // get autocompletion when triggering it on a whitespace. if (/\s/.test(str.original.charAt(tagEnd))) { @@ -205,7 +206,19 @@ export class Element { } if (this.isSelfclosing) { - transform(this.str, this.startTagStart, this.startTagEnd, [ + // The transformation is the whole start tag + <, ex: ' && + (transformEnd === this.tagNameEnd || transformEnd === this.tagNameEnd + 1) + ) { + transformEnd = this.startTagStart; + this.str.remove(this.startTagStart, this.startTagStart + 1); + } + + transform(this.str, this.startTagStart, transformEnd, [ // Named slot transformations go first inside a outer block scope because //
means "use the x of let:x", and without a separate // block scope this would give a "used before defined" error @@ -295,7 +308,7 @@ export class Element { default: { createElementStatement = [ `${createElement}("`, - [this.node.start + 1, this.node.start + 1 + this.node.name.length], + [this.node.start + 1, this.tagNameEnd], `"${addActions()}, {` ]; break; diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts index 66f6d0818..2be599802 100644 --- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts +++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/InlineComponent.ts @@ -38,6 +38,7 @@ export class InlineComponent { private startTagStart: number; private startTagEnd: number; private isSelfclosing: boolean; + private tagNameEnd: number; public child?: any; // Add const $$xxx = ... only if the variable name is actually used @@ -64,7 +65,7 @@ export class InlineComponent { this.startTagStart = this.node.start; this.startTagEnd = this.computeStartTagEnd(); - const tagEnd = this.startTagStart + this.node.name.length + 1; + const tagEnd = (this.tagNameEnd = this.startTagStart + this.node.name.length + 1); // Ensure deleted characters are mapped to the attributes object so we // get autocompletion when triggering it on a whitespace. if (/\s/.test(str.original.charAt(tagEnd))) { @@ -227,7 +228,7 @@ export class InlineComponent { if (endStart === -1) { // Can happen in loose parsing mode when there's no closing tag endStart = this.node.end; - this.startTagEnd = this.node.end - 1; + this.startTagEnd = Math.max(this.node.end - 1, this.tagNameEnd); } else { endStart += this.node.start; } @@ -238,7 +239,17 @@ export class InlineComponent { } this.endTransformation.push('}'); - transform(this.str, this.startTagStart, this.startTagEnd, [ + let transformationEnd = this.startTagEnd; + + // The transformation is the whole start tag + <, ex: \nhttps://svelte.dev/e/expected_token", + "filename": "(unknown)", + "start": { + "line": 7, + "column": 2, + "character": 138 + }, + "end": { + "line": 7, + "column": 2, + "character": 138 + }, + "position": [ + 138, + 138 + ], + "frame": " 5:
\n 6: \n ^\n 8: \n 9: +
+ + +\nhttps://svelte.dev/e/expected_token", + "filename": "(unknown)", + "start": { + "line": 7, + "column": 2, + "character": 138 + }, + "end": { + "line": 7, + "column": 2, + "character": 138 + }, + "position": [ + 138, + 138 + ], + "frame": " 5:
\n 6: \n ^\n 8: \n 9: +
+ + + Date: Fri, 2 May 2025 22:41:30 +0800 Subject: [PATCH 7/7] fix: invalidate module resolution when higher priority file is added (#2754) part of #2738 --- .../src/plugins/typescript/module-loader.ts | 70 ++++++++++++++----- .../src/plugins/typescript/service.ts | 19 ++--- .../features/DiagnosticsProvider.test.ts | 2 +- 3 files changed, 63 insertions(+), 28 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 6c96ec172..a681c658a 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -164,8 +164,13 @@ export function createSvelteModuleLoader( >(); const impliedNodeFormatResolver = new ImpliedNodeFormatResolver(tsSystem); - const failedPathToContainingFile = new FileMap(); - const failedLocationInvalidated = new FileSet(); + const resolutionWithFailedLookup = new Set< + ts.ResolvedModuleWithFailedLookupLocations & { + files?: Set; + } + >(); + const failedLocationInvalidated = new FileSet(tsSystem.useCaseSensitiveFileNames); + const pendingFailedLocationCheck = new FileSet(tsSystem.useCaseSensitiveFileNames); return { svelteFileExists: svelteSys.svelteFileExists, @@ -179,16 +184,7 @@ export function createSvelteModuleLoader( deleteUnresolvedResolutionsFromCache: (path: string) => { svelteSys.deleteFromCache(path); moduleCache.deleteUnresolvedResolutionsFromCache(path); - - const previousTriedButFailed = failedPathToContainingFile.get(path); - - if (!previousTriedButFailed) { - return; - } - - for (const containingFile of previousTriedButFailed) { - failedLocationInvalidated.add(containingFile); - } + pendingFailedLocationCheck.add(path); tsModuleCache.clear(); typeReferenceCache.clear(); @@ -197,7 +193,8 @@ export function createSvelteModuleLoader( resolveTypeReferenceDirectiveReferences, mightHaveInvalidatedResolutions, clearPendingInvalidations, - getModuleResolutionCache: () => tsModuleCache + getModuleResolutionCache: () => tsModuleCache, + invalidateFailedLocationResolution }; function resolveModuleNames( @@ -222,11 +219,7 @@ export function createSvelteModuleLoader( options ); - resolvedModule?.failedLookupLocations?.forEach((failedLocation) => { - const failedPaths = failedPathToContainingFile.get(failedLocation) ?? new FileSet(); - failedPaths.add(containingFile); - failedPathToContainingFile.set(failedLocation, failedPaths); - }); + cacheResolutionWithFailedLookup(resolvedModule, containingFile); moduleCache.set(moduleName, containingFile, resolvedModule?.resolvedModule); return resolvedModule?.resolvedModule; @@ -333,5 +326,46 @@ export function createSvelteModuleLoader( function clearPendingInvalidations() { moduleCache.clearPendingInvalidations(); failedLocationInvalidated.clear(); + pendingFailedLocationCheck.clear(); + } + + function cacheResolutionWithFailedLookup( + resolvedModule: ts.ResolvedModuleWithFailedLookupLocations & { + files?: Set; + }, + containingFile: string + ) { + if (!resolvedModule.failedLookupLocations?.length) { + return; + } + + // The resolvedModule object will be reused in different files. A bit hacky, but TypeScript also does this. + // https://github.com/microsoft/TypeScript/blob/11e79327598db412a161616849041487673fadab/src/compiler/resolutionCache.ts#L1103 + resolvedModule.files ??= new Set(); + resolvedModule.files.add(containingFile); + resolutionWithFailedLookup.add(resolvedModule); + } + + function invalidateFailedLocationResolution() { + resolutionWithFailedLookup.forEach((resolvedModule) => { + if ( + !resolvedModule.resolvedModule || + !resolvedModule.files || + !resolvedModule.failedLookupLocations + ) { + return; + } + for (const location of resolvedModule.failedLookupLocations) { + if (pendingFailedLocationCheck.has(location)) { + moduleCache.delete(resolvedModule.resolvedModule.resolvedFileName); + resolvedModule.files?.forEach((file) => { + failedLocationInvalidated.add(file); + }); + break; + } + } + }); + + pendingFailedLocationCheck.clear(); } } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index a45f04ecd..7ceec495e 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -89,10 +89,10 @@ declare module 'typescript' { resolutionDiagnostics?: ts.Diagnostic[]; /** * @internal - * Used to issue a diagnostic if typings for a non-relative import couldn't be found - * while respecting package.json `exports`, but were found when disabling `exports`. + * Used to issue a better diagnostic when an unresolvable module may + * have been resolvable under different module resolution settings. */ - node10Result?: string; + alternateResult?: string; } } @@ -527,10 +527,11 @@ async function createLanguageService( function invalidateModuleCache(filePaths: string[]) { for (const filePath of filePaths) { - svelteModuleLoader.deleteFromModuleCache(filePath); - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); + const normalizedPath = normalizePath(filePath); + svelteModuleLoader.deleteFromModuleCache(normalizedPath); + svelteModuleLoader.deleteUnresolvedResolutionsFromCache(normalizedPath); - scheduleUpdate(filePath); + scheduleUpdate(normalizedPath); } } @@ -974,6 +975,7 @@ async function createLanguageService( return; } + svelteModuleLoader.invalidateFailedLocationResolution(); const oldProgram = project?.program; let program: ts.Program | undefined; try { @@ -981,15 +983,14 @@ async function createLanguageService( } finally { // mark as clean even if the update fails, at least we can still try again next time there is a change dirty = false; + compilerHost = undefined; + svelteModuleLoader.clearPendingInvalidations(); } - svelteModuleLoader.clearPendingInvalidations(); if (project) { project.program = program; } - compilerHost = undefined; - if (!skipSvelteInputCheck) { const svelteConfigDiagnostics = checkSvelteInput(program, projectConfig); const codes = svelteConfigDiagnostics.map((d) => d.code); diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts index dbccee6e5..b6227bfb7 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -84,7 +84,7 @@ describe('DiagnosticsProvider', function () { try { const diagnostics3 = await plugin.getDiagnostics(document); - assert.deepStrictEqual(diagnostics3.length, 1); + assert.deepStrictEqual(diagnostics3.length, 0); await lsAndTsDocResolver.deleteSnapshot(newTsFilePath); } finally { unlinkSync(newTsFilePath);