Skip to content

Commit c5cc702

Browse files
authored
[Feature] Add Svelte Renderer (#57)
1 parent 6747d0c commit c5cc702

File tree

11 files changed

+202
-34
lines changed

11 files changed

+202
-34
lines changed

README.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,11 @@ All major frameworks are supported.
1919
color: white;
2020

2121
/* 👇 Define component's props directly in your CSS */
22-
&[data-variant="primary"] {
22+
&[data-variant='primary'] {
2323
background: blue;
2424
}
2525

26-
&[data-variant="secondary"] {
26+
&[data-variant='secondary'] {
2727
background: gray;
2828
}
2929
}
@@ -54,7 +54,7 @@ export const App = () => (
5454
)
5555
```
5656

57-
MistCSS can generate ⚛️ __React__, 💚 __Vue__, 🚀 __Astro__ and 🔥 __Hono__ components. You can use 🍃 __Tailwind CSS__ to style them.
57+
MistCSS can generate ⚛️ **React**, 💚 **Vue**, 🚀 **Astro**, 🧠**Svelte** and 🔥 **Hono** components. You can use 🍃 **Tailwind CSS** to style them.
5858

5959
## Documentation
6060

@@ -66,6 +66,7 @@ https://typicode.github.io/mistcss
6666
- [Remix](https://remix.run/)
6767
- [React](https://react.dev/)
6868
- [Vue](https://vuejs.org)
69+
- [Svelte](https://svelte.dev/)
6970
- [Astro](https://astro.build/)
7071
- [Hono](https://hono.dev/)
7172
- [Tailwind CSS](https://tailwindcss.com/)

docs/src/content/docs/integration/frameworks.mdx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ mistcss ./components --target=react
1616
mistcss ./components --target=vue
1717
```
1818

19+
## Svelte
20+
21+
```sh
22+
mistcss ./components --target=svelte
23+
```
24+
1925
## Astro
2026

2127
```sh

docs/src/content/docs/intro.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@ Supports:
2020
- [Remix](https://remix.run/)
2121
- [React](https://react.dev/)
2222
- [Vue](https://vuejs.org)
23+
- [Svelte](https://svelte.dev/)
2324
- [Astro](https://astro.build/)
2425
- [Hono](https://hono.dev/)
2526
- [Tailwind CSS](https://tailwindcss.com/)
2627

27-
__Bonus__: if you need to switch from one framework to another, you won't have to rewrite your components. Simply change MistCSS compilation target.
28+
__Bonus__: if you need to switch from one framework to another, you won't have to rewrite your components. Simply change MistCSS compilation target.

src/bin.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
#!/usr/bin/env node
2-
import fsPromises from 'node:fs/promises'
32
import fs from 'node:fs'
3+
import fsPromises from 'node:fs/promises'
44
import path from 'node:path'
55
import { parseArgs } from 'node:util'
66

77
import chokidar from 'chokidar'
88
import { globby } from 'globby'
99

1010
import { parse } from './parser.js'
11-
import { render as reactRender } from './renderers/react.js'
1211
import { render as astroRender } from './renderers/astro.js'
12+
import { render as reactRender } from './renderers/react.js'
13+
import { render as svelteRender } from './renderers/svelte.js'
1314
import { render as vueRender } from './renderers/vue.js'
1415

15-
type Extension = '.tsx' | '.astro'
16-
type Target = 'react' | 'hono' | 'astro' | 'vue';
16+
type Extension = '.tsx' | '.astro' | '.svelte'
17+
type Target = 'react' | 'hono' | 'astro' | 'vue' | 'svelte'
1718

1819
function createFile(mist: string, target: Target, ext: Extension) {
1920
try {
@@ -34,6 +35,9 @@ function createFile(mist: string, target: Target, ext: Extension) {
3435
case 'vue':
3536
result = vueRender(name, data[0])
3637
break
38+
case 'svelte':
39+
result = svelteRender(name, data[0])
40+
break
3741
}
3842
fs.writeFileSync(mist.replace(/\.css$/, ext), result)
3943
}
@@ -50,7 +54,7 @@ function createFile(mist: string, target: Target, ext: Extension) {
5054
function usage() {
5155
console.log(`Usage: mistcss <directory> [options]
5256
--watch, -w Watch for changes
53-
--target, -t Render target (react, vue, astro, hono) [default: react]
57+
--target, -t Render target (react, vue, astro, hono, svelte) [default: react]
5458
`)
5559
}
5660

@@ -84,8 +88,14 @@ if (!(await fsPromises.stat(dir)).isDirectory()) {
8488
process.exit(1)
8589
}
8690

87-
const { target } = values;
88-
if (target !== 'react' && target !== 'hono' && target !== 'astro' && target !== 'vue') {
91+
const { target } = values
92+
if (
93+
target !== 'react' &&
94+
target !== 'hono' &&
95+
target !== 'astro' &&
96+
target !== 'vue' &&
97+
target !== 'svelte'
98+
) {
8999
console.error('Invalid render option')
90100
usage()
91101
process.exit(1)
@@ -110,6 +120,10 @@ switch (target) {
110120
ext = '.tsx'
111121
console.log('Rendering Vue components')
112122
break
123+
case 'svelte':
124+
ext = '.svelte'
125+
console.log('Rendering Svelte components')
126+
break
113127
default:
114128
console.error('Invalid target option')
115129
usage()
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`render > renders Svelte component (full) 1`] = `
4+
"<script lang="ts">
5+
// Generated by MistCSS, do not modify
6+
import type { SvelteHTMLElements } from 'svelte/elements'
7+
import './component.mist.css'
8+
9+
type Props = { attr?: 'a' | 'b', attrFooBar?: 'foo-bar', isFoo?: boolean, propFoo?: string, propBar?: string } & SvelteHTMLElements['div']
10+
11+
type $$Props = Props
12+
13+
const { attr, attrFooBar, isFoo, propFoo, propBar, ...props } = $$props
14+
</script>
15+
16+
<div {...props} data-attr={attr} data-attr-foo-bar={attrFooBar} data-is-foo={isFoo} style:--prop-foo={propFoo} style:--prop-bar={propBar} class="foo" ><slot /></div>
17+
"
18+
`;
19+
20+
exports[`render > renders Svelte component (minimal) 1`] = `
21+
"<script lang="ts">
22+
// Generated by MistCSS, do not modify
23+
import type { SvelteHTMLElements } from 'svelte/elements'
24+
import './component.mist.css'
25+
26+
type Props = { } & SvelteHTMLElements['div']
27+
28+
type $$Props = Props
29+
30+
const { ...props } = $$props
31+
</script>
32+
33+
<div {...props} class="foo" ><slot /></div>
34+
"
35+
`;
36+
37+
exports[`render > renders Svelte component (void element) 1`] = `
38+
"<script lang="ts">
39+
// Generated by MistCSS, do not modify
40+
import type { SvelteHTMLElements } from 'svelte/elements'
41+
import './component.mist.css'
42+
43+
type Props = { } & SvelteHTMLElements['hr']
44+
45+
type $$Props = Props
46+
47+
const { ...props } = $$props
48+
</script>
49+
50+
<hr {...props} class="foo" />
51+
"
52+
`;

src/renderers/_common.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
21
import { Data } from '../parser.js'
2+
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
33

44
// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
55
const voidElements = new Set([
@@ -65,9 +65,30 @@ export function renderPropsInterface(data: Data, extendedType: string): string {
6565
].join(' ')
6666
}
6767

68+
function renderSvelteStyle(properties: Data['properties']): string {
69+
return Array.from(properties)
70+
.map((property) => `style:${property}={${propertyToCamelCase(property)}}`)
71+
.join(' ')
72+
}
73+
74+
function renderStyleObject(properties: Data['properties']) {
75+
return [
76+
'style={{ ',
77+
Array.from(properties)
78+
.map((property) => `'${property}': ${propertyToCamelCase(property)}`)
79+
.join(', '),
80+
' }}',
81+
].join('')
82+
}
83+
6884
// Example:
6985
// <div {...props} data-foo={dataFoo} data-bar={dataBar} style={{ '--foo': foo, '--bar': bar }} class="foo">{children}</div>
70-
export function renderTag(data: Data, slotText: string, classText: string): string {
86+
export function renderTag(
87+
data: Data,
88+
slotText: string,
89+
classText: string,
90+
styleFormat: 'object' | 'svelte',
91+
): string {
7192
return [
7293
`<${data.tag}`,
7394
'{...props}',
@@ -86,21 +107,13 @@ export function renderTag(data: Data, slotText: string, classText: string): stri
86107
.join(' ')
87108
: null,
88109
data.properties.size
89-
? [
90-
'style={{ ',
91-
Array.from(data.properties)
92-
.map(
93-
(property) => `'${property}': ${propertyToCamelCase(property)}`,
94-
)
95-
.join(', '),
96-
' }}',
97-
].join('')
110+
? styleFormat === 'object'
111+
? renderStyleObject(data.properties)
112+
: renderSvelteStyle(data.properties)
98113
: null,
99114
`${classText}="${data.className}"`,
100-
hasChildren(data.tag)
101-
? [`>${slotText}</${data.tag}>`]
102-
: '/>',
115+
hasChildren(data.tag) ? [`>${slotText}</${data.tag}>`] : '/>',
103116
]
104117
.filter((x) => x !== null)
105118
.join(' ')
106-
}
119+
}

src/renderers/astro.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
21
import { Data } from '../parser.js'
3-
import { renderTag, renderPropsInterface } from './_common.js'
2+
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
3+
import { renderPropsInterface,renderTag } from './_common.js'
44

55
function renderProps(data: Data): string {
66
return [
@@ -27,6 +27,6 @@ ${renderPropsInterface(data, `HTMLAttributes<'${data.tag}'>`)}
2727
${renderProps(data)}
2828
---
2929
30-
${renderTag(data, '<slot />', 'class')}
30+
${renderTag(data, '<slot />', 'class', 'object')}
3131
`
3232
}

src/renderers/react.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { Data } from '../parser.js'
12
import {
23
attributeToCamelCase,
34
pascalCase,
45
propertyToCamelCase,
56
} from './_case.js'
6-
import { Data } from '../parser.js'
77
import { hasChildren, renderPropsInterface, renderTag } from './_common.js'
88

99
function renderImports(data: Data, isHono: boolean): string {
@@ -35,7 +35,7 @@ function renderFunction(data: Data, isClass: boolean): string {
3535
* ${data.comment}
3636
*/
3737
export function ${pascalCase(data.className)}({ ${args} }: ${hasChildren(data.tag) ? `PropsWithChildren<Props>` : `Props`}) {
38-
return (${renderTag(data, '{children}', isClass ? 'class' : 'className')})
38+
return (${renderTag(data, '{children}', isClass ? 'class' : 'className', 'object')})
3939
}`
4040
}
4141

src/renderers/svelte.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect, it, describe } from 'vitest'
2+
3+
import { Data } from '../parser.js'
4+
import { render } from './svelte.js'
5+
6+
describe('render', () => {
7+
it('renders Svelte component (full)', () => {
8+
const data: Data = {
9+
tag: 'div',
10+
className: 'foo',
11+
attributes: {
12+
'data-attr': new Set(['a', 'b']),
13+
'data-attr-foo-bar': new Set(['foo-bar']),
14+
},
15+
booleanAttributes: new Set(['data-is-foo']),
16+
properties: new Set(['--prop-foo', '--prop-bar']),
17+
}
18+
19+
const result = render('component', data)
20+
expect(result).toMatchSnapshot()
21+
})
22+
23+
it('renders Svelte component (minimal)', () => {
24+
const data: Data = {
25+
tag: 'div',
26+
className: 'foo',
27+
attributes: {},
28+
booleanAttributes: new Set(),
29+
properties: new Set(),
30+
}
31+
32+
const result = render('component', data)
33+
expect(result).toMatchSnapshot()
34+
})
35+
36+
it('renders Svelte component (void element)', () => {
37+
const data: Data = {
38+
tag: 'hr', // hr is a void element and should not have children
39+
className: 'foo',
40+
attributes: {},
41+
booleanAttributes: new Set(),
42+
properties: new Set(),
43+
}
44+
45+
const result = render('component', data)
46+
expect(result).toMatchSnapshot()
47+
})
48+
})

src/renderers/svelte.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Data } from '../parser.js'
2+
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
3+
import { renderPropsInterface, renderTag } from './_common.js'
4+
5+
function renderProps(data: Data): string {
6+
return [
7+
'const {',
8+
[
9+
...Object.keys(data.attributes).map(attributeToCamelCase),
10+
...Array.from(data.booleanAttributes).map(attributeToCamelCase),
11+
...Array.from(data.properties).map(propertyToCamelCase),
12+
'...props',
13+
].join(', '),
14+
'} = $$props',
15+
].join(' ')
16+
}
17+
18+
export function render(filename: string, data: Data): string {
19+
return `<script lang="ts">
20+
// Generated by MistCSS, do not modify
21+
import type { SvelteHTMLElements } from 'svelte/elements'
22+
import './${filename}.mist.css'
23+
24+
${renderPropsInterface(data, `SvelteHTMLElements['${data.tag}']`)}
25+
26+
type $$Props = Props
27+
28+
${renderProps(data)}
29+
</script>
30+
31+
${renderTag(data, '<slot />', 'class', 'svelte')}
32+
`
33+
}

0 commit comments

Comments
 (0)