Repository: sveltejs/language-tools
Pull request #1443 (2022-04-11 14:14:21)

(fix) relax tag mapping for svelte:element
Relax the general case of the mapElementTag method to allow the element in <svelte:element bind:this={element} /> to be assigned to something more specific than HTMLElement
Refine and fix new transformation during that, silencing a wrong this-error and making it possible to infer union types
#1438


Patch:
diff --git a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts
index 19f432dc..4c6c9b96 100644
--- a/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx_v2/nodes/Element.ts
@@ -94,7 +94,6 @@ export class Element {
             case 'svelte:head':
             case 'svelte:window':
             case 'svelte:body':
-            case 'svelte:element':
             case 'svelte:fragment': {
                 // remove the colon: svelte:xxx -> sveltexxx
                 const nodeName = `svelte${this.node.name.substring(7)}`;
@@ -104,6 +103,21 @@ export class Element {
                     (this.startTransformation[0] = `{ const ${this._name} = ${createElement}("${nodeName}", {`);
                 break;
             }
+            case 'svelte:element': {
+                const nodeName = this.node.tag
+                    ? typeof this.node.tag !== 'string'
+                        ? ([this.node.tag.start, this.node.tag.end] as [number, number])
+                        : `"${this.node.tag}"`
+                    : '""';
+                this._name = '$$_svelteelement' + this.computeDepth();
+                this.startTransformation.push(`{ ${createElement}(`, nodeName, ', {');
+                this.addNameConstDeclaration = () => (
+                    (this.startTransformation[0] = `{ const ${this._name} = ${createElement}(`),
+                    nodeName,
+                    ', {'
+                );
+                break;
+            }
             case 'slot': {
                 // If the element is a <slot> tag, create the element with the createSlot-function
                 // which is created inside createRenderFunction.ts to check that the name and attributes
diff --git a/packages/svelte2tsx/svelte-jsx.d.ts b/packages/svelte2tsx/svelte-jsx.d.ts
index ed36e15e..d8b53b48 100644
--- a/packages/svelte2tsx/svelte-jsx.d.ts
+++ b/packages/svelte2tsx/svelte-jsx.d.ts
@@ -11,11 +11,12 @@ declare namespace svelteHTML {
   ): SVGElementTagNameMap[K];
   function mapElementTag(
     tag: any
-  ): HTMLElement;
+  ): any; // needs to be any because used in context of <svelte:element>
 
   function createElement<Elements extends IntrinsicElements, Key extends keyof Elements>(
-    element: Key, attrs: Elements[Key]
-  ): Key extends keyof ElementTagNameMap ? ElementTagNameMap[Key] : Key extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[Key] : HTMLElement;
+    // "undefined | null" because of <svelte:element>
+    element: Key | undefined | null, attrs: Elements[Key]
+  ): Key extends keyof ElementTagNameMap ? ElementTagNameMap[Key] : Key extends keyof SVGElementTagNameMap ? SVGElementTagNameMap[Key] : any;
 
 
   type NativeElement = HTMLElement;
@@ -298,7 +299,6 @@ declare namespace svelteHTML {
     sveltefragment: { slot?: string; };
     svelteoptions: { [name: string]: any };
     sveltehead: { [name: string]: any };
-    svelteelement: { 'this': string | undefined | null; } & HTMLProps<any> & SVGProps<any>;
 
     [name: string]: { [name: string]: any };
   }
diff --git a/packages/svelte2tsx/svelte-shims.d.ts b/packages/svelte2tsx/svelte-shims.d.ts
index 1da43b7f..02fc0e36 100644
--- a/packages/svelte2tsx/svelte-shims.d.ts
+++ b/packages/svelte2tsx/svelte-shims.d.ts
@@ -185,7 +185,7 @@ declare function __sveltets_1_mapElementTag<K extends keyof SVGElementTagNameMap
 ): SVGElementTagNameMap[K];
 declare function __sveltets_1_mapElementTag(
     tag: any
-): HTMLElement;
+): any; // needs to be any because used in context of <svelte:element>
 
 declare function __sveltets_1_bubbleEventDef<Events, K extends keyof Events>(
     events: Events, eventKey: K


Test patch:
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expected.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expected.json
index 57e3758f..a40274af 100644
--- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expected.json
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expected.json
@@ -1,8 +1,8 @@
 [
     {
-        "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 0 } },
+        "range": { "start": { "line": 12, "character": 0 }, "end": { "line": 12, "character": 0 } },
         "severity": 1,
-        "source": "js",
+        "source": "ts",
         "message": "<svelte:element> must have a 'this' attribute",
         "code": -1
     }
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expectedv2.json b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expectedv2.json
index 57e3758f..a40274af 100644
--- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expectedv2.json
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/expectedv2.json
@@ -1,8 +1,8 @@
 [
     {
-        "range": { "start": { "line": 10, "character": 0 }, "end": { "line": 10, "character": 0 } },
+        "range": { "start": { "line": 12, "character": 0 }, "end": { "line": 12, "character": 0 } },
         "severity": 1,
-        "source": "js",
+        "source": "ts",
         "message": "<svelte:element> must have a 'this' attribute",
         "code": -1
     }
diff --git a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/input.svelte b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/input.svelte
index ec1324d0..cf0ccdc0 100644
--- a/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/input.svelte
+++ b/packages/language-server/test/plugins/typescript/features/diagnostics/fixtures/svelte-element/input.svelte
@@ -1,11 +1,13 @@
-<script>
+<script lang="ts">
   let tag = 'div';
+  let element: HTMLAnchorElement | HTMLButtonElement;
 </script>
 
 <!-- valid -->
 <svelte:element this={tag} />
 <svelte:element this={tag}>{tag}</svelte:element>
 <svelte:element this={tag} on:click={() => tag} />
+<svelte:element this={tag} bind:this={element} />
 
 <!-- error -->
 <svelte:element />
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expected.tsx
index 134e6073..d82ed0b5 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expected.tsx
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expected.tsx
@@ -6,6 +6,8 @@
 () => (<>
 
 <svelteelement this={tag} />
+<svelteelement this="tag" />
+<svelteelement this={tag ? 'a' : 'b'} />
 <svelteelement this={tag}>{tag}</svelteelement>
 <svelteelement this={tag} onclick={() => tag} /></>);
 return { props: {}, slots: {}, getters: {}, events: {} }}
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expectedv2.ts b/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expectedv2.ts
index 312059a8..64bbe6b0 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expectedv2.ts
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/expectedv2.ts
@@ -5,9 +5,11 @@
 ;
 async () => {
 
- { svelteHTML.createElement("svelteelement", { });}
- { svelteHTML.createElement("svelteelement", { });tag; }
- { svelteHTML.createElement("svelteelement", {   "onclick":() => tag,});}};
+ { svelteHTML.createElement(tag, {  });}
+ { svelteHTML.createElement("tag", { });}
+ { svelteHTML.createElement(tag ? 'a' : 'b', {  });}
+ { svelteHTML.createElement(tag, { });tag; }
+ { svelteHTML.createElement(tag, {    "onclick":() => tag,});}};
 return { props: {}, slots: {}, getters: {}, events: {} }}
 
 export default class Input__SvelteComponent_ extends __sveltets_1_createSvelte2TsxComponent(__sveltets_1_partial(__sveltets_1_with_any_event(render()))) {
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/input.svelte
index e0c57b76..7fe6fc40 100644
--- a/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/input.svelte
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/svelte-element/input.svelte
@@ -3,5 +3,7 @@
 </script>
 
 <svelte:element this={tag} />
+<svelte:element this="tag" />
+<svelte:element this={tag ? 'a' : 'b'} />
 <svelte:element this={tag}>{tag}</svelte:element>
 <svelte:element this={tag} on:click={() => tag} />