@@ -60,6 +60,21 @@ export async function transformCss(
6060 } )
6161
6262 let output = result . css
63+
64+ // PostCSS doesn't add semicolons to at-rules without bodies when they're the last node.
65+ // We need to manually ensure they have semicolons.
66+ const root = result . root
67+ if ( root . nodes && root . nodes . length > 0 ) {
68+ const lastNode = root . nodes [ root . nodes . length - 1 ]
69+ if (
70+ lastNode . type === "atrule" &&
71+ ! lastNode . nodes &&
72+ ! output . trimEnd ( ) . endsWith ( ";" )
73+ ) {
74+ output = output . trimEnd ( ) + ";"
75+ }
76+ }
77+
6378 output = output . replace ( / \/ \* - - - b r e a k - - - \* \/ / g, "" )
6479 output = output . replace ( / ( \n \s * \n ) + / g, "\n\n" )
6580 output = output . trimEnd ( )
@@ -79,20 +94,77 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
7994
8095 const [ , name , params ] = atRuleMatch
8196
82- // Special handling for plugins - place them after imports
83- if ( name === "plugin " ) {
84- // Find existing plugin with same params
85- const existingPlugin = root . nodes ?. find (
97+ // Special handling for imports - place them at the top.
98+ if ( name === "import " ) {
99+ // Check if this import already exists.
100+ const existingImport = root . nodes ?. find (
86101 ( node ) : node is AtRule =>
87102 node . type === "atrule" &&
88- node . name === "plugin " &&
103+ node . name === "import " &&
89104 node . params === params
90105 )
91106
107+ if ( ! existingImport ) {
108+ const importRule = postcss . atRule ( {
109+ name : "import" ,
110+ params,
111+ raws : { semicolon : true } ,
112+ } )
113+
114+ // Find the last import to insert after, or insert at beginning.
115+ const importNodes = root . nodes ?. filter (
116+ ( node ) : node is AtRule =>
117+ node . type === "atrule" && node . name === "import"
118+ )
119+
120+ if ( importNodes && importNodes . length > 0 ) {
121+ // Insert after the last existing import.
122+ const lastImport = importNodes [ importNodes . length - 1 ]
123+ importRule . raws . before = "\n"
124+ root . insertAfter ( lastImport , importRule )
125+ } else {
126+ // No imports exist, insert at the very beginning.
127+ // Check if the file is empty.
128+ if ( ! root . nodes || root . nodes . length === 0 ) {
129+ importRule . raws . before = ""
130+ } else {
131+ importRule . raws . before = ""
132+ }
133+ root . prepend ( importRule )
134+ }
135+ }
136+ }
137+ // Special handling for plugins - place them after imports.
138+ else if ( name === "plugin" ) {
139+ // Ensure plugin name is quoted if not already.
140+ let quotedParams = params
141+ if ( params && ! params . startsWith ( '"' ) && ! params . startsWith ( "'" ) ) {
142+ quotedParams = `"${ params } "`
143+ }
144+
145+ // Normalize params for comparison (remove quotes).
146+ const normalizeParams = ( p : string ) => {
147+ if ( p . startsWith ( '"' ) && p . endsWith ( '"' ) ) {
148+ return p . slice ( 1 , - 1 )
149+ }
150+ if ( p . startsWith ( "'" ) && p . endsWith ( "'" ) ) {
151+ return p . slice ( 1 , - 1 )
152+ }
153+ return p
154+ }
155+
156+ // Find existing plugin with same normalized params.
157+ const existingPlugin = root . nodes ?. find ( ( node ) : node is AtRule => {
158+ if ( node . type !== "atrule" || node . name !== "plugin" ) {
159+ return false
160+ }
161+ return normalizeParams ( node . params ) === normalizeParams ( params )
162+ } )
163+
92164 if ( ! existingPlugin ) {
93165 const pluginRule = postcss . atRule ( {
94166 name : "plugin" ,
95- params,
167+ params : quotedParams ,
96168 raws : { semicolon : true , before : "\n" } ,
97169 } )
98170
@@ -141,7 +213,34 @@ function updateCssPlugin(css: z.infer<typeof registryItemCssSchema>) {
141213 }
142214 }
143215 }
144- // Special handling for keyframes - place them under @theme inline
216+ // Check if this is any at-rule with no body (empty object).
217+ else if (
218+ typeof properties === "object" &&
219+ Object . keys ( properties ) . length === 0
220+ ) {
221+ // Handle any at-rule with no body (e.g., @apply, @tailwind, etc.).
222+ const atRule = root . nodes ?. find (
223+ ( node ) : node is AtRule =>
224+ node . type === "atrule" &&
225+ node . name === name &&
226+ node . params === params
227+ ) as AtRule | undefined
228+
229+ if ( ! atRule ) {
230+ const newAtRule = postcss . atRule ( {
231+ name,
232+ params,
233+ raws : { semicolon : true } ,
234+ } )
235+
236+ root . append ( newAtRule )
237+ root . insertBefore (
238+ newAtRule ,
239+ postcss . comment ( { text : "---break---" } )
240+ )
241+ }
242+ }
243+ // Special handling for keyframes - place them under @theme inline.
145244 else if ( name === "keyframes" ) {
146245 let themeInline = root . nodes ?. find (
147246 ( node ) : node is AtRule =>
@@ -339,25 +438,43 @@ function processRule(parent: Root | AtRule, selector: string, properties: any) {
339438
340439 if ( typeof properties === "object" ) {
341440 for ( const [ prop , value ] of Object . entries ( properties ) ) {
342- if ( typeof value === "string" ) {
441+ // Check if this is any at-rule with empty object (no body).
442+ if (
443+ prop . startsWith ( "@" ) &&
444+ typeof value === "object" &&
445+ value !== null &&
446+ Object . keys ( value ) . length === 0
447+ ) {
448+ // Parse the at-rule.
449+ const atRuleMatch = prop . match ( / @ ( [ a - z A - Z - ] + ) \s * ( .* ) / )
450+ if ( atRuleMatch ) {
451+ const [ , atRuleName , atRuleParams ] = atRuleMatch
452+ const atRule = postcss . atRule ( {
453+ name : atRuleName ,
454+ params : atRuleParams ,
455+ raws : { semicolon : true , before : "\n " } ,
456+ } )
457+ rule . append ( atRule )
458+ }
459+ } else if ( typeof value === "string" ) {
343460 const decl = postcss . decl ( {
344461 prop,
345462 value : value ,
346463 raws : { semicolon : true , before : "\n " } ,
347464 } )
348465
349- // Replace existing property or add new one
466+ // Replace existing property or add new one.
350467 const existingDecl = rule . nodes ?. find (
351468 ( node ) : node is Declaration =>
352469 node . type === "decl" && node . prop === prop
353470 )
354471
355472 existingDecl ? existingDecl . replaceWith ( decl ) : rule . append ( decl )
356473 } else if ( typeof value === "object" ) {
357- // Nested selector (including & selectors)
474+ // Nested selector (including & selectors).
358475 const nestedSelector = prop . startsWith ( "&" )
359476 ? selector . replace ( / ^ ( [ ^ : ] + ) / , `$1${ prop . substring ( 1 ) } ` )
360- : prop // Use the original selector for other nested elements
477+ : prop // Use the original selector for other nested elements.
361478 processRule ( parent , nestedSelector , value )
362479 }
363480 }
0 commit comments