Repository: sveltejs/language-tools
Pull request #937 (2021-04-06 12:10:45)

Support foreign namespace behaviour in language-server and svelte2tsx
This allows for case sensitive attributes in the JSX via the `foreign` namespace. Which then allows `svelte-nodegui` and `svelte-native` to use the correct casing of attribute names and avoid errors at runtime.

This involved wiring through the svelte compiler config from the language server into svelte2tsx then into htmlxtojsx

Bonus feature, I can run `yarn test` on windows due to `cross-env`


Patch:
diff --git a/package.json b/package.json
index e83e1589..d0983695 100644
--- a/package.json
+++ b/package.json
@@ -10,7 +10,7 @@
     "scripts": {
         "bootstrap": "yarn workspace svelte2tsx build && yarn workspace svelte-vscode build:grammar",
         "build": "tsc -b",
-        "test": "CI=true yarn workspaces run test",
+        "test": "cross-env CI=true yarn workspaces run test",
         "watch": "tsc -b -watch",
         "format": "prettier --write .",
         "lint": "prettier --check . && eslint \"packages/**/*.{ts,js}\""
@@ -25,6 +25,7 @@
         "eslint": "^7.7.0",
         "eslint-plugin-import": "^2.22.1",
         "eslint-plugin-svelte3": "^2.7.3",
-        "prettier": "2.2.1"
+        "prettier": "2.2.1",
+        "cross-env": "^7.0.2"
     }
 }
diff --git a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
index ff9fd2ad..c02f8b82 100644
--- a/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
+++ b/packages/language-server/src/plugins/typescript/DocumentSnapshot.ts
@@ -176,7 +176,8 @@ function preprocessSvelteFile(document: Document, options: SvelteSnapshotOptions
             strictMode: options.strictMode,
             filename: document.getFilePath() ?? undefined,
             isTsFile: scriptKind === ts.ScriptKind.TSX,
-            emitOnTemplateError: options.transformOnTemplateError
+            emitOnTemplateError: options.transformOnTemplateError,
+            namespace: document.config?.compilerOptions?.namespace
         });
         text = tsx.code;
         tsxMap = tsx.map;
diff --git a/packages/svelte2tsx/index.d.ts b/packages/svelte2tsx/index.d.ts
index e0a27783..68a9f0cd 100644
--- a/packages/svelte2tsx/index.d.ts
+++ b/packages/svelte2tsx/index.d.ts
@@ -36,5 +36,9 @@ export default function svelte2tsx(
          * Whether to try emitting result when there's a syntax error in the template
          */
         emitOnTemplateError?: boolean;
+        /**
+         * The namespace option from svelte config
+         */
+        namespace?: string;
     }
 ): SvelteCompiledToTsx
diff --git a/packages/svelte2tsx/src/htmlxtojsx/index.ts b/packages/svelte2tsx/src/htmlxtojsx/index.ts
index dd2fe7b1..f650a8f7 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/index.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/index.ts
@@ -45,7 +45,8 @@ export function convertHtmlxToJsx(
     str: MagicString,
     ast: TemplateNode,
     onWalk: Walker = null,
-    onLeave: Walker = null
+    onLeave: Walker = null,
+    options: { preserveAttributeCase?: boolean } = {}
 ): void {
     const htmlx = str.original;
     stripDoctype(str);
@@ -139,7 +140,13 @@ export function convertHtmlxToJsx(
                         handleAnimateDirective(htmlx, str, node as BaseDirective, parent);
                         break;
                     case 'Attribute':
-                        handleAttribute(htmlx, str, node as Attribute, parent);
+                        handleAttribute(
+                            htmlx,
+                            str,
+                            node as Attribute,
+                            parent,
+                            options.preserveAttributeCase
+                        );
                         break;
                     case 'EventHandler':
                         handleEventHandler(htmlx, str, node as BaseDirective, parent);
@@ -218,11 +225,14 @@ export function convertHtmlxToJsx(
 /**
  * @internal For testing only
  */
-export function htmlx2jsx(htmlx: string, options?: { emitOnTemplateError?: boolean }) {
+export function htmlx2jsx(
+    htmlx: string,
+    options?: { emitOnTemplateError?: boolean; preserveAttributeCase: boolean }
+) {
     const ast = parseHtmlx(htmlx, options);
     const str = new MagicString(htmlx);
 
-    convertHtmlxToJsx(str, ast);
+    convertHtmlxToJsx(str, ast, null, null, options);
 
     return {
         map: str.generateMap({ hires: true }),
diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts
index 95eda94c..f2801b27 100644
--- a/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts
+++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/attribute.ts
@@ -39,10 +39,19 @@ export function handleAttribute(
     htmlx: string,
     str: MagicString,
     attr: Attribute,
-    parent: BaseNode
+    parent: BaseNode,
+    preserveCase: boolean
 ): void {
     let transformedFromDirectiveOrNamespace = false;
 
+    const transformAttributeCase = (name: string) => {
+        if (!preserveCase && !svgAttributes.find((x) => x == name)) {
+            return name.toLowerCase();
+        } else {
+            return name;
+        }
+    };
+
     //if we are on an "element" we are case insensitive, lowercase to match our JSX
     if (parent.type == 'Element') {
         const sapperLinkActions = ['sapper:prefetch', 'sapper:noscroll'];
@@ -58,10 +67,7 @@ export function handleAttribute(
             sapperLinkActions.includes(attr.name) ||
             sveltekitLinkActions.includes(attr.name)
         ) {
-            let name = attr.name;
-            if (!svgAttributes.find((x) => x == name)) {
-                name = name.toLowerCase();
-            }
+            let name = transformAttributeCase(attr.name);
 
             //strip ":" from out attribute name and uppercase the next letter to convert to jsx attribute
             const colonIndex = name.indexOf(':');
@@ -83,7 +89,7 @@ export function handleAttribute(
             !transformedFromDirectiveOrNamespace &&
             parent.name !== '!DOCTYPE'
         ) {
-            str.overwrite(attr.start, attr.end, attr.name.toLowerCase());
+            str.overwrite(attr.start, attr.end, transformAttributeCase(attr.name));
         }
         return;
     }
@@ -101,10 +107,7 @@ export function handleAttribute(
         if (attrVal.type == 'AttributeShorthand') {
             let attrName = attrVal.expression.name;
             if (parent.type == 'Element') {
-                // eslint-disable-next-line max-len
-                attrName = svgAttributes.find((a) => a == attrName)
-                    ? attrName
-                    : attrName.toLowerCase();
+                attrName = transformAttributeCase(attrName);
             }
 
             str.appendRight(attr.start, `${attrName}=`);
diff --git a/packages/svelte2tsx/src/svelte2tsx/index.ts b/packages/svelte2tsx/src/svelte2tsx/index.ts
index 87b1c051..cb7c2f93 100644
--- a/packages/svelte2tsx/src/svelte2tsx/index.ts
+++ b/packages/svelte2tsx/src/svelte2tsx/index.ts
@@ -74,7 +74,7 @@ const COMPONENT_SUFFIX = '__SvelteComponent_';
 
 function processSvelteTemplate(
     str: MagicString,
-    options?: { emitOnTemplateError?: boolean }
+    options?: { emitOnTemplateError?: boolean; namespace?: string }
 ): TemplateProcessResult {
     const htmlxAst = parseHtmlx(str.original, options);
 
@@ -287,7 +287,9 @@ function processSvelteTemplate(
         }
     };
 
-    convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave);
+    convertHtmlxToJsx(str, htmlxAst, onHtmlxWalk, onHtmlxLeave, {
+        preserveAttributeCase: options.namespace == 'foreign'
+    });
 
     // resolve scripts
     const { scriptTag, moduleScriptTag } = scripts.getTopLevelScriptTags();
@@ -458,6 +460,7 @@ export function svelte2tsx(
         strictMode?: boolean;
         isTsFile?: boolean;
         emitOnTemplateError?: boolean;
+        namespace?: string;
     }
 ) {
     const str = new MagicString(svelte);


Test patch:
diff --git a/packages/svelte2tsx/test/helpers.ts b/packages/svelte2tsx/test/helpers.ts
index 96016006..9d456af7 100644
--- a/packages/svelte2tsx/test/helpers.ts
+++ b/packages/svelte2tsx/test/helpers.ts
@@ -199,6 +199,7 @@ type TransformSampleFn = (
         filename: string;
         sampleName: string;
         emitOnTemplateError: boolean;
+        preserveAttributeCase: boolean;
     }
 ) => ReturnType<typeof htmlx2jsx | typeof svelte2tsx>;
 
@@ -214,7 +215,8 @@ export function test_samples(dir: string, transform: TransformSampleFn, jsx: 'js
         const config = {
             filename: svelteFile,
             sampleName: sample.name,
-            emitOnTemplateError: false
+            emitOnTemplateError: false,
+            preserveAttributeCase: sample.name.endsWith('-foreign-ns')
         };
 
         if (process.env.CI) {
@@ -295,7 +297,8 @@ export function get_svelte2tsx_config(base: BaseConfig, sampleName: string): Sve
         filename: base.filename,
         emitOnTemplateError: base.emitOnTemplateError,
         strictMode: sampleName.includes('strictMode'),
-        isTsFile: sampleName.startsWith('ts-')
+        isTsFile: sampleName.startsWith('ts-'),
+        namespace: sampleName.endsWith('-foreign-ns') ? 'foreign' : null
     };
 }
 
diff --git a/packages/svelte2tsx/test/htmlx2jsx/index.ts b/packages/svelte2tsx/test/htmlx2jsx/index.ts
index d6d89347..d3c26465 100644
--- a/packages/svelte2tsx/test/htmlx2jsx/index.ts
+++ b/packages/svelte2tsx/test/htmlx2jsx/index.ts
@@ -4,8 +4,8 @@ import { test_samples } from '../helpers';
 describe('htmlx2jsx', () => {
     test_samples(
         __dirname,
-        (input, { emitOnTemplateError }) => {
-            return htmlx2jsx(input, { emitOnTemplateError });
+        (input, { emitOnTemplateError, preserveAttributeCase }) => {
+            return htmlx2jsx(input, { emitOnTemplateError, preserveAttributeCase });
         },
         'jsx'
     );
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/attribute-foreign-ns/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/attribute-foreign-ns/expected.jsx
new file mode 100644
index 00000000..e82ef5c0
--- /dev/null
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/attribute-foreign-ns/expected.jsx
@@ -0,0 +1,2 @@
+<><SomeComponent attrName="text" attrCase="text" />
+<someelement attrName="text" attrCase /></>
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/attribute-foreign-ns/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/attribute-foreign-ns/input.svelte
new file mode 100644
index 00000000..fe145bbe
--- /dev/null
+++ b/packages/svelte2tsx/test/htmlx2jsx/samples/attribute-foreign-ns/input.svelte
@@ -0,0 +1,2 @@
+<SomeComponent attrName="text" attrCase=text />
+<someelement attrName="text" attrCase />
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/attributes-foreign-ns/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/attributes-foreign-ns/expected.tsx
new file mode 100644
index 00000000..793e8a3d
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/attributes-foreign-ns/expected.tsx
@@ -0,0 +1,8 @@
+///<reference types="svelte" />
+<></>;function render() {
+<><element someAttr="hi" someOtherAttribute="there">hello</element>
+<Component someAttr="5" otherAttr={6} /></>
+return { props: {}, slots: {}, getters: {}, events: {} }}
+
+export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(__sveltets_with_any_event(render))) {
+}
\ No newline at end of file
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/attributes-foreign-ns/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/attributes-foreign-ns/input.svelte
new file mode 100644
index 00000000..a48242b2
--- /dev/null
+++ b/packages/svelte2tsx/test/svelte2tsx/samples/attributes-foreign-ns/input.svelte
@@ -0,0 +1,2 @@
+<element someAttr="hi" someOtherAttribute="there">hello</element>
+<Component someAttr="5" otherAttr={6} />
\ No newline at end of file