Skip to content

Commit

Permalink
(feat) support for svelte:fragment (#848)
Browse files Browse the repository at this point in the history
  • Loading branch information
dummdidumm committed Mar 2, 2021
1 parent 55c2e91 commit eac5c74
Show file tree
Hide file tree
Showing 17 changed files with 167 additions and 92 deletions.
4 changes: 2 additions & 2 deletions packages/language-server/package.json
Expand Up @@ -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": "*",
Expand Down
11 changes: 11 additions & 0 deletions packages/language-server/src/plugins/html/dataProvider.ts
Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion packages/svelte2tsx/README.md
Expand Up @@ -23,7 +23,6 @@ Input.svelte
</script>
<h1>hello {world}</h1>
```

will produce this ugly but type checkable TSX
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte2tsx/package.json
Expand Up @@ -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"
Expand Down
10 changes: 8 additions & 2 deletions packages/svelte2tsx/src/htmlxtojsx/index.ts
Expand Up @@ -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';
Expand All @@ -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;

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
77 changes: 5 additions & 72 deletions 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 `<svelte:self>` 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);
Expand All @@ -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);
}
9 changes: 8 additions & 1 deletion 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(
','
Expand Down
61 changes: 61 additions & 0 deletions 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, '</>}}');
}
2 changes: 1 addition & 1 deletion packages/svelte2tsx/src/htmlxtojsx/nodes/svelte-tag.ts
Expand Up @@ -3,7 +3,7 @@ import { Node } from 'estree-walker';

/**
* `<svelte:window>...</svelte:window>` ----> `<sveltewindow>...</sveltewindow>`
* (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);
Expand Down
6 changes: 4 additions & 2 deletions packages/svelte2tsx/svelte-jsx.d.ts
Expand Up @@ -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 & {
Expand Down Expand Up @@ -952,6 +953,7 @@ declare namespace svelte.JSX {
// Svelte specific
sveltewindow: HTMLProps<Window> & SvelteWindowProps;
sveltebody: HTMLProps<HTMLElement>;
sveltefragment: { slot?: string; };

[name: string]: { [name: string]: any };
}
Expand Down
@@ -0,0 +1,8 @@
<><Parent >{() => { let {foo, bar:baz} = __sveltets_instanceOf(Parent).$$slot_def['default'];<>
{() => { let {bla} = __sveltets_instanceOf(Parent).$$slot_def['named'];<><Component >
{foo} {baz} {bla}
</Component></>}}
<Component >{() => { let {blubb} = __sveltets_instanceOf(Component).$$slot_def['default'];<>
{blubb}
</>}}</Component>
</>}}</Parent></>
@@ -0,0 +1,8 @@
<Parent let:foo let:bar={baz}>
<Component slot="named" let:bla>
{foo} {baz} {bla}
</Component>
<Component let:blubb>
{blubb}
</Component>
</Parent>
@@ -0,0 +1,19 @@
<><Component>
<sveltefragment>
<p>hi</p>
</sveltefragment>

<sveltefragment >
<p>hi</p>
</sveltefragment>
</Component>

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

{() => { let {foo, bar:baz} = __sveltets_instanceOf(Component).$$slot_def['named'];<><sveltefragment >
<p>{foo} {baz}</p>
</sveltefragment></>}}
</Component></>
@@ -0,0 +1,19 @@
<Component>
<svelte:fragment>
<p>hi</p>
</svelte:fragment>

<svelte:fragment slot="named">
<p>hi</p>
</svelte:fragment>
</Component>

<Component>
<svelte:fragment let:foo let:bar={baz}>
<p>{foo} {baz}</p>
</svelte:fragment>

<svelte:fragment slot="named" let:foo let:bar={baz}>
<p>{foo} {baz}</p>
</svelte:fragment>
</Component>
Expand Up @@ -11,7 +11,8 @@
<sveltehead>
<h1>Hi</h1>
</sveltehead>
<svelteoptions /></>
<svelteoptions />
<sveltefragment /></>
return { props: {}, slots: {}, getters: {}, events: {} }}

export default class Input__SvelteComponent_ extends createSvelte2TsxComponent(__sveltets_partial(__sveltets_with_any_event(render))) {
Expand Down
Expand Up @@ -9,4 +9,5 @@
<svelte:head>
<h1>Hi</h1>
</svelte:head>
<svelte:options />
<svelte:options />
<svelte:fragment />
16 changes: 8 additions & 8 deletions yarn.lock
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down

0 comments on commit eac5c74

Please sign in to comment.