Skip to content
Open
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
9 changes: 9 additions & 0 deletions playground/components/DemoButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ defineProps({
</template>

<style scoped lang="ts">
const test = keyFrames({
from: {
transform: 'scale(1)'
},
to: {
transform: 'scale(1.2)'
}
})
css({
'.demo-button': {
'--button-primary': (props) => `{color.${props.color}.600}`,
Expand All @@ -26,6 +34,7 @@ css({
transition: '{transition.all}',
color: '{color.white}',
boxShadow: `0 5px 0 {button.primary}, 0 12px 16px {color.dimmed}`,
animation: `3s linear 1s ${test} infinite`,
span: {
display: 'inline-block',
fontFamily: '{font.secondary}',
Expand Down
64 changes: 59 additions & 5 deletions src/transforms/css.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { ASTNode } from 'ast-types'
import { defu } from 'defu'
import { parse } from 'acorn'
import type { PinceauContext } from '../types'
import { resolveCssProperty, stringify } from '../utils'
import { hash } from 'ohash'
import type { AnimationAst, PinceauContext } from '../types'
import { resolveCssProperty, stringify, stringifyKeyFrames } from '../utils'
import { message } from '../utils/logger'
import { parseAst, printAst, visitAst } from '../utils/ast'
import { resolveRuntimeContents } from './vue/computed'

function genTransformError(e, id: string, loc?: any) {
e.loc.line = (loc.start.line + e.loc.line) - 1
const filePath = `${id.split('?')[0]}:${e.loc.line}:${e.loc.column}`
message('TRANSFORM_ERROR', [filePath, e])
}

/**
* Stringify every call of css() into a valid Vue <style> declaration.
*/
Expand All @@ -22,9 +29,7 @@ export function transformCssFunction(id: string,
parse(code, { ecmaVersion: 'latest' })
}
catch (e) {
e.loc.line = (loc.start.line + e.loc.line) - 1
const filePath = `${id.split('?')[0]}:${e.loc.line}:${e.loc.column}`
message('TRANSFORM_ERROR', [filePath, e])
genTransformError(e, id, loc)
return ''
}

Expand All @@ -43,6 +48,19 @@ export function transformCssFunction(id: string,
return stringify(declaration, (property: any, value: any, _style: any, _selectors: any) => resolveCssProperty(property, value, _style, _selectors, Object.keys(localTokens || {}), ctx, loc))
}

export function transformKeyFrameFunction(id: string, code = '', loc?: any) {
try {
parse(code, { ecmaVersion: 'latest' })
}
catch (e) {
genTransformError(e, id, loc)
return ''
}

const declaration = resolveKeyFrameCallees(code, ast => evalKeyframeDeclaration(ast))
return stringifyKeyFrames(declaration)
}

/**
* Transform a variants property to nested selectors.
*/
Expand Down Expand Up @@ -71,6 +89,23 @@ export function resolveCssCallees(code: string, cb: (ast: ASTNode) => any): any
return result
}

export function resolveKeyFrameCallees(code: string, cb: (body: AnimationAst) => any): any {
const ast = parseAst(code)
let result: any = false
visitAst(ast, {
visitCallExpression(path: any) {
if (path.value.callee.name === 'keyFrames') {
result = defu(result || {}, cb({
animationName: path?.parentPath.value.id.name,
animationCode: path.value.arguments[0],
}))
}
return this.traverse(path)
},
})
return result
}

/**
* Resolve computed styles found in css() declaration.
*/
Expand All @@ -92,3 +127,22 @@ export function evalCssDeclaration(cssAst: ASTNode, computedStyles: any = {}, lo
return {}
}
}

export function evalKeyframeDeclaration(body: AnimationAst) {
try {
const { animationCode, animationName } = body
// eslint-disable-next-line no-eval
const _eval = eval
const keyFramesCode = printAst(animationCode).code
const keyFrameName = `pa-${hash(keyFramesCode)}`

_eval(`var ${animationName} = '${keyFrameName}'`)
_eval(`var keyFrameDeclaration = ${keyFramesCode}`)

// @ts-expect-error - Evaluated code
return { keyFrameDeclaration, keyFrameName }
}
catch (error) {
return {}
}
}
12 changes: 7 additions & 5 deletions src/transforms/vue/sfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { parseVueComponent } from '../../utils/ast'
import type { PinceauContext, PinceauQuery } from '../../types'
import { variantsRegex } from '../../utils'
import { transformDtHelper } from '../dt'
import { transformCssFunction } from '../css'
import { transformCssFunction, transformKeyFrameFunction } from '../css'
import { message } from '../../utils/logger'
import { transformStyle } from './style'
import { transformVariants } from './variants'
Expand Down Expand Up @@ -53,11 +53,11 @@ export function transformVueSFC(
*/
export function resolveStyleQuery(code: string, magicString: MagicString, query: PinceauQuery, ctx: PinceauContext, loc?: any) {
// Handle `lang="ts"` even though that should not happen here.
if (query.lang === 'ts') { code = transformCssFunction(query.id, code, {}, {}, {}, ctx, loc) }

if (query.lang === 'ts') {
code = transformCssFunction(query.id, code, {}, {}, {}, ctx, loc)
}
// Transform <style> block
code = transformStyle(code, ctx)

return { code, magicString }
}

Expand Down Expand Up @@ -101,16 +101,18 @@ export function resolveStyle(
(styleBlock) => {
const { loc, content } = styleBlock
let code = content

let keyframeCode = ''
if (
styleBlock.attrs.lang === 'ts'
|| styleBlock.lang === 'ts'
|| styleBlock.attrs?.transformed
) {
keyframeCode = transformKeyFrameFunction(id, code, { query, ...loc })
code = transformCssFunction(id, code, variants, computedStyles, localTokens, ctx, { query, ...loc })
}

code = transformStyle(code, ctx)
code = keyframeCode + code

magicString.remove(loc.start.offset, loc.end.offset)
magicString.appendRight(loc.end.offset, `\n${code}\n`)
Expand Down
6 changes: 6 additions & 0 deletions src/types/css.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ASTNode } from 'ast-types'
import type * as Utils from './utils'
import type { PinceauMediaQueries, PinceauUtils } from './theme'
import type { DefaultThemeMap } from './map'
Expand Down Expand Up @@ -65,3 +66,8 @@ export type CSSFunctionType<
{
[K in string]: CSSProperties<ComponentProps> | never
}

export interface AnimationAst {
animationName: string
animationCode: ASTNode
}
13 changes: 13 additions & 0 deletions src/utils/stringify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,3 +149,16 @@ export function stringify(value,

return parse(value, [], [])
}

export function stringifyKeyFrames(value) {
if (!value) { return '' }
let cssText = ''
const { keyFrameDeclaration, keyFrameName } = value
const animationKey = keyFrameName
const animateRule: string[] = []
Object.entries(keyFrameDeclaration).forEach(([key, content]) => {
animateRule.push(`${key} ${JSON.stringify(content).replace(/'|"/g, '')}`)
})
cssText = `@keyframes ${animationKey} { ${animateRule.join(' ')} } `
return cssText || ''
}
8 changes: 6 additions & 2 deletions src/utils/vue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFileSync } from 'node:fs'
import { transformCssFunction, transformStyle } from '../transforms'
import { transformCssFunction, transformKeyFrameFunction, transformStyle } from '../transforms'
import type { PinceauContext, PinceauQuery } from '../types'
import { parseVueComponent } from './ast'

Expand All @@ -25,7 +25,11 @@ export function loadVueStyle(query: PinceauQuery, ctx: PinceauContext) {

const loc = { query, ...style.loc }

if (style.attrs.lang === 'ts') { source = transformCssFunction(query.id, source, undefined, undefined, undefined, ctx, loc) }
if (style.attrs.lang === 'ts') {
const keyFrameCode = transformKeyFrameFunction(query.id, source, loc)
const cssCode = transformCssFunction(query.id, source, undefined, undefined, undefined, ctx, loc)
source = keyFrameCode + cssCode
}

return transformStyle(source, ctx, loc)
}
1 change: 1 addition & 0 deletions src/volar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const plugin: VueLanguagePlugin = _ => ({
'\ntype OmittedKeysPinceau = \'onVnodeBeforeMount\' | \'onVnodeBeforeUnmount\' | \'onVnodeBeforeUpdate\' | \'onVnodeMounted\' | \'onVnodeUnmounted\' | \'onVnodeUpdated\' | \'key\' | \'ref\' | \'ref_for\' | \'ref_key\' | \'style\' | \'class\'\n',
`\ntype PinceauProps = Omit<InstanceType<typeof import('${fileName}').default>['$props'], OmittedKeysPinceau>\n`,
'\nfunction css (declaration: CSSFunctionType<PinceauProps>) { return { declaration } }\n',
'\nfunction keyframes(declartion: Record<string, any>) { return declaration }',
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declartion -> declaration maybe?

]
embeddedFile.content.push(...context)

Expand Down