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 (