diff --git a/packages/language-server/package.json b/packages/language-server/package.json index d1543e688..b088b07e2 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -51,9 +51,9 @@ "estree-walker": "^2.0.1", "lodash": "^4.17.19", "prettier": "2.2.1", - "prettier-plugin-svelte": "~2.1.0", + "prettier-plugin-svelte": "~2.2.0", "source-map": "^0.7.3", - "svelte": "~3.32.1", + "svelte": "~3.35.0", "svelte-preprocess": "~4.6.1", "svelte2tsx": "*", "typescript": "*", diff --git a/packages/language-server/src/plugins/html/dataProvider.ts b/packages/language-server/src/plugins/html/dataProvider.ts index 14947face..047d76803 100644 --- a/packages/language-server/src/plugins/html/dataProvider.ts +++ b/packages/language-server/src/plugins/html/dataProvider.ts @@ -176,6 +176,17 @@ const svelteTags: ITagData[] = [ } ] }, + { + name: 'svelte:fragment', + description: + 'This element is useful if you want to assign a component to a named slot without creating a wrapper DOM element.', + attributes: [ + { + name: 'slot', + description: 'The name of the named slot that should be targeted.' + } + ] + }, { name: 'slot', description: diff --git a/packages/svelte2tsx/README.md b/packages/svelte2tsx/README.md index f15f4a000..0870c0405 100644 --- a/packages/svelte2tsx/README.md +++ b/packages/svelte2tsx/README.md @@ -23,7 +23,6 @@ Input.svelte

hello {world}

- ``` will produce this ugly but type checkable TSX diff --git a/packages/svelte2tsx/package.json b/packages/svelte2tsx/package.json index 085eda340..dcd5ad31a 100644 --- a/packages/svelte2tsx/package.json +++ b/packages/svelte2tsx/package.json @@ -32,7 +32,7 @@ "@rollup/plugin-typescript": "^6.0.0", "source-map": "^0.6.1", "source-map-support": "^0.5.16", - "svelte": "~3.32.1", + "svelte": "~3.35.0", "tiny-glob": "^0.2.6", "tslib": "^1.10.0", "typescript": "^4.2.2" diff --git a/packages/svelte2tsx/src/htmlxtojsx/index.ts b/packages/svelte2tsx/src/htmlxtojsx/index.ts index 059c903d6..b5eb8ad87 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/index.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/index.ts @@ -11,6 +11,7 @@ import { handleBinding } from './nodes/binding'; import { handleClassDirective } from './nodes/class-directive'; import { handleComment } from './nodes/comment'; import { handleComponent } from './nodes/component'; +import { handleSlot } from './nodes/slot'; import { handleDebug } from './nodes/debug'; import { handleEach } from './nodes/each'; import { handleElement } from './nodes/element'; @@ -20,6 +21,7 @@ import { handleRawHtml } from './nodes/raw-html'; import { handleSvelteTag } from './nodes/svelte-tag'; import { handleTransitionDirective } from './nodes/transition-directive'; import { handleText } from './nodes/text'; +import { getSlotName } from '../utils/svelteAst'; type Walker = (node: Node, parent: Node, prop: string, index: number) => void; @@ -72,10 +74,10 @@ export function convertHtmlxToJsx( handleDebug(htmlx, str, node); break; case 'InlineComponent': - handleComponent(htmlx, str, node); + handleComponent(htmlx, str, node, parent); break; case 'Element': - handleElement(htmlx, str, node); + handleElement(htmlx, str, node, parent); break; case 'Comment': handleComment(str, node); @@ -113,6 +115,10 @@ export function convertHtmlxToJsx( case 'Body': handleSvelteTag(htmlx, str, node); break; + case 'SlotTemplate': + handleSvelteTag(htmlx, str, node); + handleSlot(htmlx, str, node, parent, getSlotName(node) || 'default'); + break; case 'Text': handleText(str, node); break; diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts index 5c8fc4803..cfb611ac8 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/component.ts @@ -1,13 +1,12 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; import { getSlotName } from '../../utils/svelteAst'; -import { beforeStart } from '../utils/node-utils'; -import { getSingleSlotDef } from '../../svelte2tsx/nodes/slot'; +import { handleSlot } from './slot'; /** * Handle `` and slot-specific transformations. */ -export function handleComponent(htmlx: string, str: MagicString, el: Node): void { +export function handleComponent(htmlx: string, str: MagicString, el: Node, parent: Node): void { //we need to remove : if it is a svelte component if (el.name.startsWith('svelte:')) { const colon = htmlx.indexOf(':', el.start); @@ -20,73 +19,7 @@ export function handleComponent(htmlx: string, str: MagicString, el: Node): void } } - //we only need to do something if there is a let or slot - handleSlot(htmlx, str, el, el, 'default'); - - //walk the direct children looking for slots. We do this here because we need the name of our component for handleSlot - //we could lean on leave/enter, but I am lazy - if (!el.children) return; - for (const child of el.children) { - const slotName = getSlotName(child); - if (slotName) { - handleSlot(htmlx, str, child, el, slotName); - } - } -} - -function handleSlot( - htmlx: string, - str: MagicString, - slotEl: Node, - component: Node, - slotName: string -): void { - //collect "let" definitions - const slotElIsComponent = slotEl === component; - let hasMoved = false; - let slotDefInsertionPoint: number; - for (const attr of slotEl.attributes) { - if (attr.type != 'Let') { - continue; - } - - if (slotElIsComponent && slotEl.children.length == 0) { - //no children anyway, just wipe out the attribute - str.remove(attr.start, attr.end); - continue; - } - - slotDefInsertionPoint = - slotDefInsertionPoint || - (slotElIsComponent - ? htmlx.lastIndexOf('>', slotEl.children[0].start) + 1 - : slotEl.start); - - str.move(attr.start, attr.end, slotDefInsertionPoint); - - //remove let: - if (hasMoved) { - str.overwrite(attr.start, attr.start + 'let:'.length, ', '); - } else { - str.remove(attr.start, attr.start + 'let:'.length); - } - hasMoved = true; - if (attr.expression) { - //overwrite the = as a : - const equalSign = htmlx.lastIndexOf('=', attr.expression.start); - const curly = htmlx.lastIndexOf('{', beforeStart(attr.expression.start)); - str.overwrite(equalSign, curly + 1, ':'); - str.remove(attr.expression.end, attr.end); - } - } - if (!hasMoved) { - return; - } - str.appendLeft(slotDefInsertionPoint, '{() => { let {'); - str.appendRight(slotDefInsertionPoint, `} = ${getSingleSlotDef(component, slotName)}` + ';<>'); - - const closeSlotDefInsertionPoint = slotElIsComponent - ? htmlx.lastIndexOf('<', slotEl.end - 1) - : slotEl.end; - str.appendLeft(closeSlotDefInsertionPoint, '}}'); + // Handle possible slot + const slotName = getSlotName(el) || 'default'; + handleSlot(htmlx, str, el, slotName === 'default' ? el : parent, slotName); } diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts index dbd0a6335..79094ad0c 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/element.ts @@ -1,10 +1,17 @@ import MagicString from 'magic-string'; import { Node } from 'estree-walker'; +import { getSlotName } from '../../utils/svelteAst'; +import { handleSlot } from './slot'; /** * Special treatment for self-closing / void tags to make them conform to JSX. */ -export function handleElement(htmlx: string, str: MagicString, node: Node): void { +export function handleElement(htmlx: string, str: MagicString, node: Node, parent: Node): void { + const slotName = getSlotName(node); + if (slotName) { + handleSlot(htmlx, str, node, parent, slotName); + } + //we just have to self close void tags since jsx always wants the /> const voidTags = 'area,base,br,col,embed,hr,img,input,link,meta,param,source,track,wbr'.split( ',' diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts new file mode 100644 index 000000000..5e6634119 --- /dev/null +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/slot.ts @@ -0,0 +1,61 @@ +import MagicString from 'magic-string'; +import { Node } from 'estree-walker'; +import { beforeStart } from '../utils/node-utils'; +import { getSingleSlotDef } from '../../svelte2tsx/nodes/slot'; + +export function handleSlot( + htmlx: string, + str: MagicString, + slotEl: Node, + component: Node, + slotName: string +): void { + //collect "let" definitions + const slotElIsComponent = slotEl === component; + let hasMoved = false; + let slotDefInsertionPoint: number; + for (const attr of slotEl.attributes) { + if (attr.type != 'Let') { + continue; + } + + if (slotElIsComponent && slotEl.children.length == 0) { + //no children anyway, just wipe out the attribute + str.remove(attr.start, attr.end); + continue; + } + + slotDefInsertionPoint = + slotDefInsertionPoint || + (slotElIsComponent + ? htmlx.lastIndexOf('>', slotEl.children[0].start) + 1 + : slotEl.start); + + str.move(attr.start, attr.end, slotDefInsertionPoint); + + //remove let: + if (hasMoved) { + str.overwrite(attr.start, attr.start + 'let:'.length, ', '); + } else { + str.remove(attr.start, attr.start + 'let:'.length); + } + hasMoved = true; + if (attr.expression) { + //overwrite the = as a : + const equalSign = htmlx.lastIndexOf('=', attr.expression.start); + const curly = htmlx.lastIndexOf('{', beforeStart(attr.expression.start)); + str.overwrite(equalSign, curly + 1, ':'); + str.remove(attr.expression.end, attr.end); + } + } + if (!hasMoved) { + return; + } + str.appendLeft(slotDefInsertionPoint, '{() => { let {'); + str.appendRight(slotDefInsertionPoint, `} = ${getSingleSlotDef(component, slotName)}` + ';<>'); + + const closeSlotDefInsertionPoint = slotElIsComponent + ? htmlx.lastIndexOf('<', slotEl.end - 1) + : slotEl.end; + str.appendLeft(closeSlotDefInsertionPoint, '}}'); +} diff --git a/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts b/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts index 19f0c063c..1474f8da2 100644 --- a/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts +++ b/packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts @@ -3,7 +3,7 @@ import { Node } from 'estree-walker'; /** * `...` ----> `...` - * (same for :head, :body, :options) + * (same for :head, :body, :options, :fragment) */ export function handleSvelteTag(htmlx: string, str: MagicString, node: Node): void { const colon = htmlx.indexOf(':', node.start); diff --git a/packages/svelte2tsx/svelte-jsx.d.ts b/packages/svelte2tsx/svelte-jsx.d.ts index af890c268..3f0364bbf 100644 --- a/packages/svelte2tsx/svelte-jsx.d.ts +++ b/packages/svelte2tsx/svelte-jsx.d.ts @@ -28,8 +28,9 @@ declare namespace svelte.JSX { type NativeElement = HTMLElement; - // eslint-disable-next-line @typescript-eslint/no-empty-interface - interface IntrinsicAttributes {} + interface IntrinsicAttributes { + slot?: string; + } // TypeScript SVGElement has no `dataset` (Chrome 55+, Firefox 51+). type Element = NativeElement & { @@ -952,6 +953,7 @@ declare namespace svelte.JSX { // Svelte specific sveltewindow: HTMLProps & SvelteWindowProps; sveltebody: HTMLProps; + sveltefragment: { slot?: string; }; [name: string]: { [name: string]: any }; } diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/component-named-slot/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/component-named-slot/expected.jsx new file mode 100644 index 000000000..aa99fcbcd --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/component-named-slot/expected.jsx @@ -0,0 +1,8 @@ +<>{() => { let {foo, bar:baz} = __sveltets_instanceOf(Parent).$$slot_def['default'];<> + {() => { let {bla} = __sveltets_instanceOf(Parent).$$slot_def['named'];<> + {foo} {baz} {bla} + }} + {() => { let {blubb} = __sveltets_instanceOf(Component).$$slot_def['default'];<> + {blubb} + }} +}} \ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/component-named-slot/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/component-named-slot/input.svelte new file mode 100644 index 000000000..e31be2e58 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/component-named-slot/input.svelte @@ -0,0 +1,8 @@ + + + {foo} {baz} {bla} + + + {blubb} + + diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/svelte-fragment/expected.jsx b/packages/svelte2tsx/test/htmlx2jsx/samples/svelte-fragment/expected.jsx new file mode 100644 index 000000000..564e2275a --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/svelte-fragment/expected.jsx @@ -0,0 +1,19 @@ +<> + +

hi

+
+ + +

hi

+
+
+ + + {() => { let {foo, bar:baz} = __sveltets_instanceOf(Component).$$slot_def['default'];<> +

{foo} {baz}

+
}} + + {() => { let {foo, bar:baz} = __sveltets_instanceOf(Component).$$slot_def['named'];<> +

{foo} {baz}

+
}} +
\ No newline at end of file diff --git a/packages/svelte2tsx/test/htmlx2jsx/samples/svelte-fragment/input.svelte b/packages/svelte2tsx/test/htmlx2jsx/samples/svelte-fragment/input.svelte new file mode 100644 index 000000000..c65321779 --- /dev/null +++ b/packages/svelte2tsx/test/htmlx2jsx/samples/svelte-fragment/input.svelte @@ -0,0 +1,19 @@ + + +

hi

+
+ + +

hi

+
+
+ + + +

{foo} {baz}

+
+ + +

{foo} {baz}

+
+
diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/expected.tsx b/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/expected.tsx index 02e12227e..4f692d2bb 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/expected.tsx +++ b/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/expected.tsx @@ -11,7 +11,8 @@

Hi

- + + return { props: {}, slots: {}, getters: {}, events: {} }} export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(__sveltets_with_any_event(render))) { diff --git a/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/input.svelte b/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/input.svelte index c6f0b29cd..addcbd904 100644 --- a/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/input.svelte +++ b/packages/svelte2tsx/test/svelte2tsx/samples/uses-svelte-components/input.svelte @@ -9,4 +9,5 @@

Hi

- \ No newline at end of file + + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8f441c8e3..deb51db2c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2108,10 +2108,10 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== -prettier-plugin-svelte@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.1.0.tgz#61b930107cf4eb8bdae0e9d416e47fb36ee6b53c" - integrity sha512-AeGJWicKCU9CbPKj9Wzk7apdCJwB8gzFHOMMqJh1X4LiwkMLHUjjysowH+SZfHdg69Hjv5rw5M7uJn0WobFhRQ== +prettier-plugin-svelte@~2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/prettier-plugin-svelte/-/prettier-plugin-svelte-2.2.0.tgz#4bd94992fa5b76413a8a5556f90b128c4fdaf7a6" + integrity sha512-Xdmqgr71tAuMqqzNCK52/v94g/Yv7V7lz+nmbO9NEA+9ol15VV3uUHOfydMNOo3SWvFaVlBcp947ebEaMWqVfQ== prettier@2.2.1: version "2.2.1" @@ -2508,10 +2508,10 @@ svelte-preprocess@~4.6.1: detect-indent "^6.0.0" strip-indent "^3.0.0" -svelte@~3.32.1: - version "3.32.1" - resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.32.1.tgz#c4b6e35517d0ed77e652cc8964ef660afa2f70f3" - integrity sha512-j1KmD2ZOU0RGq1/STDXjwfh0/eJ/Deh2NXyuz1bpR9eOcz9yImn4CGxXdbSAN7cMTm9a7IyPUIbuBCzu/pXK0g== +svelte@~3.35.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.35.0.tgz#e0d0ba60c4852181c2b4fd851194be6fda493e65" + integrity sha512-gknlZkR2sXheu/X+B7dDImwANVvK1R0QGQLd8CNIfxxGPeXBmePnxfzb6fWwTQRsYQG7lYkZXvpXJvxvpsoB7g== table@^5.2.3: version "5.4.6"