diff --git a/packages/sv/lib/addons/tailwindcss/index.ts b/packages/sv/lib/addons/tailwindcss/index.ts index 8b5d7a34..8f0e505b 100644 --- a/packages/sv/lib/addons/tailwindcss/index.ts +++ b/packages/sv/lib/addons/tailwindcss/index.ts @@ -1,6 +1,7 @@ import { defineAddon, defineAddonOptions } from '../../core/index.ts'; import { imports, vite } from '../../core/tooling/js/index.ts'; import * as svelte from '../../core/tooling/svelte/index.ts'; +import * as css from '../../core/tooling/css/index.ts'; import { parseCss, parseJson, parseScript, parseSvelte } from '../../core/tooling/parsers.ts'; const plugins = [ @@ -59,38 +60,28 @@ export default defineAddon({ }); sv.file(files.stylesheet, (content) => { - let atRules = parseCss(content).ast.nodes.filter((node) => node.type === 'atrule'); - - const findAtRule = (name: string, params: string) => - atRules.find( - (rule) => - rule.name === name && - // checks for both double and single quote variants - rule.params.replace(/['"]/g, '') === params - ); - - let code = content; - const importsTailwind = findAtRule('import', 'tailwindcss'); - if (!importsTailwind) { - code = "@import 'tailwindcss';\n" + code; - // reparse to account for the newly added tailwindcss import - atRules = parseCss(code).ast.nodes.filter((node) => node.type === 'atrule'); - } + const { ast, generateCode } = parseCss(content); - const lastAtRule = atRules.findLast((rule) => ['plugin', 'import'].includes(rule.name)); - const pluginPos = lastAtRule!.source!.end!.offset; + // since we are prepending all the `AtRule` let's add them in reverse order, + // so they appear in the expected order in the final file for (const plugin of plugins) { if (!options.plugins.includes(plugin.id)) continue; - const pluginRule = findAtRule('plugin', plugin.package); - if (!pluginRule) { - const pluginImport = `\n@plugin '${plugin.package}';`; - code = code.substring(0, pluginPos) + pluginImport + code.substring(pluginPos); - } + css.addAtRule(ast, { + name: 'plugin', + params: `'${plugin.package}'`, + append: false + }); } - return code; + css.addAtRule(ast, { + name: 'import', + params: `'tailwindcss'`, + append: false + }); + + return generateCode(); }); if (!kit) { diff --git a/packages/sv/lib/core/tests/css/common/add-at-rule/output.css b/packages/sv/lib/core/tests/css/common/add-at-rule/output.css index e2a2d26f..bfc44e2c 100644 --- a/packages/sv/lib/core/tests/css/common/add-at-rule/output.css +++ b/packages/sv/lib/core/tests/css/common/add-at-rule/output.css @@ -1,5 +1,7 @@ @tailwind 'lib/path/file.ext'; + .foo { color: red; } + @tailwind 'lib/path/file1.ext'; diff --git a/packages/sv/lib/core/tests/css/common/add-at-rule/run.ts b/packages/sv/lib/core/tests/css/common/add-at-rule/run.ts index 83fb331d..f868f77e 100644 --- a/packages/sv/lib/core/tests/css/common/add-at-rule/run.ts +++ b/packages/sv/lib/core/tests/css/common/add-at-rule/run.ts @@ -1,6 +1,7 @@ -import { addAtRule, type CssAst } from '../../../../tooling/css/index.ts'; +import { addAtRule } from '../../../../tooling/css/index.ts'; +import { type SvelteAst } from '../../../../tooling/index.ts'; -export function run(ast: CssAst): void { +export function run(ast: SvelteAst.CSS.StyleSheet): void { addAtRule(ast, { name: 'tailwind', params: "'lib/path/file.ext'", append: false }); addAtRule(ast, { name: 'tailwind', params: "'lib/path/file1.ext'", append: true }); } diff --git a/packages/sv/lib/core/tests/css/common/add-comment/input.css b/packages/sv/lib/core/tests/css/common/add-comment/input.css deleted file mode 100644 index cedf0a6d..00000000 --- a/packages/sv/lib/core/tests/css/common/add-comment/input.css +++ /dev/null @@ -1,3 +0,0 @@ -.foo { - color: red; -} diff --git a/packages/sv/lib/core/tests/css/common/add-comment/output.css b/packages/sv/lib/core/tests/css/common/add-comment/output.css deleted file mode 100644 index fbb03f63..00000000 --- a/packages/sv/lib/core/tests/css/common/add-comment/output.css +++ /dev/null @@ -1,4 +0,0 @@ -.foo { - color: red; -} -/* foo comment */ diff --git a/packages/sv/lib/core/tests/css/common/add-comment/run.ts b/packages/sv/lib/core/tests/css/common/add-comment/run.ts deleted file mode 100644 index f9293c32..00000000 --- a/packages/sv/lib/core/tests/css/common/add-comment/run.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { addComment, type CssAst } from '../../../../tooling/css/index.ts'; - -export function run(ast: CssAst): void { - addComment(ast, { value: 'foo comment' }); -} diff --git a/packages/sv/lib/core/tests/css/common/add-imports/output.css b/packages/sv/lib/core/tests/css/common/add-imports/output.css index 4f96db19..38dadd87 100644 --- a/packages/sv/lib/core/tests/css/common/add-imports/output.css +++ b/packages/sv/lib/core/tests/css/common/add-imports/output.css @@ -1,4 +1,5 @@ @import 'lib/path/file.css'; + .foo { color: red; } diff --git a/packages/sv/lib/core/tests/css/common/add-imports/run.ts b/packages/sv/lib/core/tests/css/common/add-imports/run.ts index 71c0c222..d908e3c3 100644 --- a/packages/sv/lib/core/tests/css/common/add-imports/run.ts +++ b/packages/sv/lib/core/tests/css/common/add-imports/run.ts @@ -1,6 +1,7 @@ -import { addImports, type CssAst } from '../../../../tooling/css/index.ts'; +import { addImports } from '../../../../tooling/css/index.ts'; +import { type SvelteAst } from '../../../../tooling/index.ts'; -export function run(ast: CssAst): void { +export function run(ast: SvelteAst.CSS.StyleSheet): void { addImports(ast, { imports: ["'lib/path/file.css'"] }); diff --git a/packages/sv/lib/core/tests/css/common/add-rule/output.css b/packages/sv/lib/core/tests/css/common/add-rule/output.css index c99b655a..ff123948 100644 --- a/packages/sv/lib/core/tests/css/common/add-rule/output.css +++ b/packages/sv/lib/core/tests/css/common/add-rule/output.css @@ -1,6 +1,7 @@ .foo { color: red; } + .bar { color: blue; } diff --git a/packages/sv/lib/core/tests/css/common/add-rule/run.ts b/packages/sv/lib/core/tests/css/common/add-rule/run.ts index 75e2642b..f0ed7ff3 100644 --- a/packages/sv/lib/core/tests/css/common/add-rule/run.ts +++ b/packages/sv/lib/core/tests/css/common/add-rule/run.ts @@ -1,8 +1,9 @@ -import { addDeclaration, addRule, type CssAst } from '../../../../tooling/css/index.ts'; +import { addDeclaration, addRule } from '../../../../tooling/css/index.ts'; +import { type SvelteAst } from '../../../../tooling/index.ts'; -export function run(ast: CssAst): void { +export function run(ast: SvelteAst.CSS.StyleSheet): void { const barSelectorRule = addRule(ast, { - selector: '.bar' + selector: 'bar' }); addDeclaration(barSelectorRule, { property: 'color', diff --git a/packages/sv/lib/core/tooling/css/index.ts b/packages/sv/lib/core/tooling/css/index.ts index 565ba768..ec2a3b73 100644 --- a/packages/sv/lib/core/tooling/css/index.ts +++ b/packages/sv/lib/core/tooling/css/index.ts @@ -1,76 +1,142 @@ -import { Declaration, Rule, AtRule, Comment, type CssAst, type CssChildNode } from '../index.ts'; - -export type { CssAst }; - -export function addRule(node: CssAst, options: { selector: string }): Rule { - const rules = node.nodes.filter((x): x is Rule => x.type === 'rule'); - let rule = rules.find((x) => x.selector === options.selector); +import type { SvelteAst } from '../index.ts'; + +export function addRule( + node: SvelteAst.CSS.StyleSheet, + options: { selector: string } +): SvelteAst.CSS.Rule { + // we do not check for existing rules here, as the selector AST from svelte is really complex + const rules = node.children.filter((x) => x.type === 'Rule'); + let rule = rules.find((x) => { + const selector = x.prelude.children[0].children[0].selectors[0]; + return selector.type === 'ClassSelector' && selector.name === options.selector; + }); if (!rule) { - rule = new Rule(); - rule.selector = options.selector; - node.nodes.push(rule); + rule = { + type: 'Rule', + prelude: { + type: 'SelectorList', + children: [ + { + type: 'ComplexSelector', + children: [ + { + type: 'RelativeSelector', + selectors: [ + { + type: 'ClassSelector', + name: options.selector, + start: 0, + end: 0 + } + ], + combinator: null, + start: 0, + end: 0 + } + ], + start: 0, + end: 0 + } + ], + start: 0, + end: 0 + }, + block: { type: 'Block', children: [], start: 0, end: 0 }, + start: 0, + end: 0 + }; + node.children.push(rule); } return rule; } export function addDeclaration( - node: Rule | CssAst, + node: SvelteAst.CSS.Rule, options: { property: string; value: string } ): void { - const declarations = node.nodes.filter((x): x is Declaration => x.type === 'decl'); - let declaration = declarations.find((x) => x.prop === options.property); + const declarations = node.block.children.filter((x) => x.type === 'Declaration'); + let declaration = declarations.find((x) => x.property === options.property); if (!declaration) { - declaration = new Declaration({ prop: options.property, value: options.value }); - node.append(declaration); + declaration = { + type: 'Declaration', + property: options.property, + value: options.value, + start: 0, + end: 0 + }; + node.block.children.push(declaration); } else { declaration.value = options.value; } } -export function addImports(node: Rule | CssAst, options: { imports: string[] }): CssChildNode[] { - let prev: CssChildNode | undefined; - const nodes = options.imports.map((param) => { - const found = node.nodes.find( - (x) => x.type === 'atrule' && x.name === 'import' && x.params === param - ); - - if (found) return (prev = found); +export function addImports(node: SvelteAst.CSS.StyleSheet, options: { imports: string[] }): void { + let lastImportIndex = -1; - const rule = new AtRule({ name: 'import', params: param }); - if (prev) node.insertAfter(prev, rule); - else node.prepend(rule); + // Find the last existing @import to insert after it + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; + if (child.type === 'Atrule' && child.name === 'import') { + lastImportIndex = i; + } + } - return (prev = rule); - }); + for (const param of options.imports) { + const found = node.children.find( + (x) => x.type === 'Atrule' && x.name === 'import' && x.prelude === param + ); - return nodes; + if (found) continue; + + const atRule: SvelteAst.CSS.Atrule = { + type: 'Atrule', + name: 'import', + prelude: param, + block: null, + start: 0, + end: 0 + }; + + if (lastImportIndex >= 0) { + // Insert after the last @import + lastImportIndex++; + node.children.splice(lastImportIndex, 0, atRule); + } else { + // No existing imports, prepend at the start + node.children.unshift(atRule); + lastImportIndex = 0; + } + } } export function addAtRule( - node: CssAst, + node: SvelteAst.CSS.StyleSheet, options: { name: string; params: string; append: boolean } -): AtRule { - const atRules = node.nodes.filter((x): x is AtRule => x.type === 'atrule'); - let atRule = atRules.find((x) => x.name === options.name && x.params === options.params); +): SvelteAst.CSS.Atrule { + const atRules = node.children.filter((x) => x.type === 'Atrule'); + let atRule = atRules.find((x) => x.name === options.name && x.prelude === options.params); if (atRule) { return atRule; } - atRule = new AtRule({ name: options.name, params: options.params }); + atRule = { + type: 'Atrule', + name: options.name, + prelude: options.params, + block: null, + start: 0, + end: 0 + }; + if (!options.append) { - node.prepend(atRule); + node.children.unshift(atRule); } else { - node.append(atRule); + node.children.push(atRule); } return atRule; } - -export function addComment(node: CssAst, options: { value: string }): void { - const comment = new Comment({ text: options.value }); - node.append(comment); -} diff --git a/packages/sv/lib/core/tooling/index.ts b/packages/sv/lib/core/tooling/index.ts index 9fb28cfa..bf31c4db 100644 --- a/packages/sv/lib/core/tooling/index.ts +++ b/packages/sv/lib/core/tooling/index.ts @@ -3,15 +3,6 @@ import type { TsEstree } from './js/ts-estree.ts'; import { Document, Element, type ChildNode } from 'domhandler'; import { ElementType, parseDocument } from 'htmlparser2'; import serializeDom from 'dom-serializer'; -import { - Root as CssAst, - Declaration, - Rule, - AtRule, - Comment, - parse as postcssParse, - type ChildNode as CssChildNode -} from 'postcss'; import * as fleece from 'silver-fleece'; import { print as esrapPrint } from 'esrap'; import ts from 'esrap/languages/ts'; @@ -27,13 +18,6 @@ export { Element as HtmlElement, ElementType as HtmlElementType, - // css - CssAst, - Declaration, - Rule, - AtRule, - Comment, - // ast walker Walker }; @@ -44,10 +28,7 @@ export type { SvelteAst, // js - TsEstree as AstTypes, - - //css - CssChildNode + TsEstree as AstTypes }; /** @@ -115,12 +96,30 @@ export function serializeScript( return code; } -export function parseCss(content: string): CssAst { - return postcssParse(content); +export function parseCss(content: string): SvelteAst.CSS.StyleSheet { + const ast = parseSvelte(``); + return ast.css!; } -export function serializeCss(ast: CssAst): string { - return ast.toString(); +export function serializeCss(ast: SvelteAst.CSS.StyleSheet): string { + // `svelte` can print the stylesheet directly. But this adds the style tags (