Skip to content

Commit

Permalink
[Feature] Add Svelte Renderer (#57)
Browse files Browse the repository at this point in the history
  • Loading branch information
totto2727 authored Sep 30, 2024
1 parent 6747d0c commit c5cc702
Show file tree
Hide file tree
Showing 11 changed files with 202 additions and 34 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ All major frameworks are supported.
color: white;

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

&[data-variant="secondary"] {
&[data-variant='secondary'] {
background: gray;
}
}
Expand Down Expand Up @@ -54,7 +54,7 @@ export const App = () => (
)
```

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

## Documentation

Expand All @@ -66,6 +66,7 @@ https://typicode.github.io/mistcss
- [Remix](https://remix.run/)
- [React](https://react.dev/)
- [Vue](https://vuejs.org)
- [Svelte](https://svelte.dev/)
- [Astro](https://astro.build/)
- [Hono](https://hono.dev/)
- [Tailwind CSS](https://tailwindcss.com/)
6 changes: 6 additions & 0 deletions docs/src/content/docs/integration/frameworks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ mistcss ./components --target=react
mistcss ./components --target=vue
```

## Svelte

```sh
mistcss ./components --target=svelte
```

## Astro

```sh
Expand Down
3 changes: 2 additions & 1 deletion docs/src/content/docs/intro.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ Supports:
- [Remix](https://remix.run/)
- [React](https://react.dev/)
- [Vue](https://vuejs.org)
- [Svelte](https://svelte.dev/)
- [Astro](https://astro.build/)
- [Hono](https://hono.dev/)
- [Tailwind CSS](https://tailwindcss.com/)

__Bonus__: if you need to switch from one framework to another, you won't have to rewrite your components. Simply change MistCSS compilation target.
__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 changes: 21 additions & 7 deletions src/bin.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
#!/usr/bin/env node
import fsPromises from 'node:fs/promises'
import fs from 'node:fs'
import fsPromises from 'node:fs/promises'
import path from 'node:path'
import { parseArgs } from 'node:util'

import chokidar from 'chokidar'
import { globby } from 'globby'

import { parse } from './parser.js'
import { render as reactRender } from './renderers/react.js'
import { render as astroRender } from './renderers/astro.js'
import { render as reactRender } from './renderers/react.js'
import { render as svelteRender } from './renderers/svelte.js'
import { render as vueRender } from './renderers/vue.js'

type Extension = '.tsx' | '.astro'
type Target = 'react' | 'hono' | 'astro' | 'vue';
type Extension = '.tsx' | '.astro' | '.svelte'
type Target = 'react' | 'hono' | 'astro' | 'vue' | 'svelte'

function createFile(mist: string, target: Target, ext: Extension) {
try {
Expand All @@ -34,6 +35,9 @@ function createFile(mist: string, target: Target, ext: Extension) {
case 'vue':
result = vueRender(name, data[0])
break
case 'svelte':
result = svelteRender(name, data[0])
break
}
fs.writeFileSync(mist.replace(/\.css$/, ext), result)
}
Expand All @@ -50,7 +54,7 @@ function createFile(mist: string, target: Target, ext: Extension) {
function usage() {
console.log(`Usage: mistcss <directory> [options]
--watch, -w Watch for changes
--target, -t Render target (react, vue, astro, hono) [default: react]
--target, -t Render target (react, vue, astro, hono, svelte) [default: react]
`)
}

Expand Down Expand Up @@ -84,8 +88,14 @@ if (!(await fsPromises.stat(dir)).isDirectory()) {
process.exit(1)
}

const { target } = values;
if (target !== 'react' && target !== 'hono' && target !== 'astro' && target !== 'vue') {
const { target } = values
if (
target !== 'react' &&
target !== 'hono' &&
target !== 'astro' &&
target !== 'vue' &&
target !== 'svelte'
) {
console.error('Invalid render option')
usage()
process.exit(1)
Expand All @@ -110,6 +120,10 @@ switch (target) {
ext = '.tsx'
console.log('Rendering Vue components')
break
case 'svelte':
ext = '.svelte'
console.log('Rendering Svelte components')
break
default:
console.error('Invalid target option')
usage()
Expand Down
52 changes: 52 additions & 0 deletions src/renderers/__snapshots__/svelte.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`render > renders Svelte component (full) 1`] = `
"<script lang="ts">
// Generated by MistCSS, do not modify
import type { SvelteHTMLElements } from 'svelte/elements'
import './component.mist.css'
type Props = { attr?: 'a' | 'b', attrFooBar?: 'foo-bar', isFoo?: boolean, propFoo?: string, propBar?: string } & SvelteHTMLElements['div']
type $$Props = Props
const { attr, attrFooBar, isFoo, propFoo, propBar, ...props } = $$props
</script>
<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>
"
`;

exports[`render > renders Svelte component (minimal) 1`] = `
"<script lang="ts">
// Generated by MistCSS, do not modify
import type { SvelteHTMLElements } from 'svelte/elements'
import './component.mist.css'
type Props = { } & SvelteHTMLElements['div']
type $$Props = Props
const { ...props } = $$props
</script>
<div {...props} class="foo" ><slot /></div>
"
`;

exports[`render > renders Svelte component (void element) 1`] = `
"<script lang="ts">
// Generated by MistCSS, do not modify
import type { SvelteHTMLElements } from 'svelte/elements'
import './component.mist.css'
type Props = { } & SvelteHTMLElements['hr']
type $$Props = Props
const { ...props } = $$props
</script>
<hr {...props} class="foo" />
"
`;
43 changes: 28 additions & 15 deletions src/renderers/_common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
import { Data } from '../parser.js'
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'

// https://html.spec.whatwg.org/multipage/syntax.html#void-elements
const voidElements = new Set([
Expand Down Expand Up @@ -65,9 +65,30 @@ export function renderPropsInterface(data: Data, extendedType: string): string {
].join(' ')
}
function renderSvelteStyle(properties: Data['properties']): string {
return Array.from(properties)
.map((property) => `style:${property}={${propertyToCamelCase(property)}}`)
.join(' ')
}
function renderStyleObject(properties: Data['properties']) {
return [
'style={{ ',
Array.from(properties)
.map((property) => `'${property}': ${propertyToCamelCase(property)}`)
.join(', '),
' }}',
].join('')
}
// Example:
// <div {...props} data-foo={dataFoo} data-bar={dataBar} style={{ '--foo': foo, '--bar': bar }} class="foo">{children}</div>
export function renderTag(data: Data, slotText: string, classText: string): string {
export function renderTag(
data: Data,
slotText: string,
classText: string,
styleFormat: 'object' | 'svelte',
): string {
return [
`<${data.tag}`,
'{...props}',
Expand All @@ -86,21 +107,13 @@ export function renderTag(data: Data, slotText: string, classText: string): stri
.join(' ')
: null,
data.properties.size
? [
'style={{ ',
Array.from(data.properties)
.map(
(property) => `'${property}': ${propertyToCamelCase(property)}`,
)
.join(', '),
' }}',
].join('')
? styleFormat === 'object'
? renderStyleObject(data.properties)
: renderSvelteStyle(data.properties)
: null,
`${classText}="${data.className}"`,
hasChildren(data.tag)
? [`>${slotText}</${data.tag}>`]
: '/>',
hasChildren(data.tag) ? [`>${slotText}</${data.tag}>`] : '/>',
]
.filter((x) => x !== null)
.join(' ')
}
}
6 changes: 3 additions & 3 deletions src/renderers/astro.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
import { Data } from '../parser.js'
import { renderTag, renderPropsInterface } from './_common.js'
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
import { renderPropsInterface,renderTag } from './_common.js'

function renderProps(data: Data): string {
return [
Expand All @@ -27,6 +27,6 @@ ${renderPropsInterface(data, `HTMLAttributes<'${data.tag}'>`)}
${renderProps(data)}
---
${renderTag(data, '<slot />', 'class')}
${renderTag(data, '<slot />', 'class', 'object')}
`
}
4 changes: 2 additions & 2 deletions src/renderers/react.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Data } from '../parser.js'
import {
attributeToCamelCase,
pascalCase,
propertyToCamelCase,
} from './_case.js'
import { Data } from '../parser.js'
import { hasChildren, renderPropsInterface, renderTag } from './_common.js'

function renderImports(data: Data, isHono: boolean): string {
Expand Down Expand Up @@ -35,7 +35,7 @@ function renderFunction(data: Data, isClass: boolean): string {
* ${data.comment}
*/
export function ${pascalCase(data.className)}({ ${args} }: ${hasChildren(data.tag) ? `PropsWithChildren<Props>` : `Props`}) {
return (${renderTag(data, '{children}', isClass ? 'class' : 'className')})
return (${renderTag(data, '{children}', isClass ? 'class' : 'className', 'object')})
}`
}

Expand Down
48 changes: 48 additions & 0 deletions src/renderers/svelte.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect, it, describe } from 'vitest'

import { Data } from '../parser.js'
import { render } from './svelte.js'

describe('render', () => {
it('renders Svelte component (full)', () => {
const data: Data = {
tag: 'div',
className: 'foo',
attributes: {
'data-attr': new Set(['a', 'b']),
'data-attr-foo-bar': new Set(['foo-bar']),
},
booleanAttributes: new Set(['data-is-foo']),
properties: new Set(['--prop-foo', '--prop-bar']),
}

const result = render('component', data)
expect(result).toMatchSnapshot()
})

it('renders Svelte component (minimal)', () => {
const data: Data = {
tag: 'div',
className: 'foo',
attributes: {},
booleanAttributes: new Set(),
properties: new Set(),
}

const result = render('component', data)
expect(result).toMatchSnapshot()
})

it('renders Svelte component (void element)', () => {
const data: Data = {
tag: 'hr', // hr is a void element and should not have children
className: 'foo',
attributes: {},
booleanAttributes: new Set(),
properties: new Set(),
}

const result = render('component', data)
expect(result).toMatchSnapshot()
})
})
33 changes: 33 additions & 0 deletions src/renderers/svelte.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Data } from '../parser.js'
import { attributeToCamelCase, propertyToCamelCase } from './_case.js'
import { renderPropsInterface, renderTag } from './_common.js'

function renderProps(data: Data): string {
return [
'const {',
[
...Object.keys(data.attributes).map(attributeToCamelCase),
...Array.from(data.booleanAttributes).map(attributeToCamelCase),
...Array.from(data.properties).map(propertyToCamelCase),
'...props',
].join(', '),
'} = $$props',
].join(' ')
}

export function render(filename: string, data: Data): string {
return `<script lang="ts">
// Generated by MistCSS, do not modify
import type { SvelteHTMLElements } from 'svelte/elements'
import './${filename}.mist.css'
${renderPropsInterface(data, `SvelteHTMLElements['${data.tag}']`)}
type $$Props = Props
${renderProps(data)}
</script>
${renderTag(data, '<slot />', 'class', 'svelte')}
`
}
6 changes: 3 additions & 3 deletions src/renderers/vue.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { attributeToCamelCase, pascalCase, propertyToCamelCase } from './_case.js'
import { Data } from '../parser.js'
import { renderTag, renderPropsInterface, hasChildren } from './_common.js'
import { attributeToCamelCase, pascalCase, propertyToCamelCase } from './_case.js'
import { hasChildren,renderPropsInterface, renderTag } from './_common.js'

function renderFunction(data: Data): string {
const args = [
Expand All @@ -14,7 +14,7 @@ function renderFunction(data: Data): string {
* ${data.comment}
*/
export function ${pascalCase(data.className)}({ ${args} }: Props${hasChildren(data.tag) ? ', { slots }: SetupContext' : ''}) {
return (${renderTag(data, '{slots.default?.()}', 'class')})
return (${renderTag(data, '{slots.default?.()}', 'class', 'object')})
}`
}

Expand Down

0 comments on commit c5cc702

Please sign in to comment.