Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add Svelte Renderer #57

Merged
merged 5 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading