Skip to content
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
41 changes: 16 additions & 25 deletions packages/sv/lib/addons/tailwindcss/index.ts
Original file line number Diff line number Diff line change
@@ -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 = [
Expand Down Expand Up @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions packages/sv/lib/core/tests/css/common/add-at-rule/output.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
@tailwind 'lib/path/file.ext';

.foo {
color: red;
}

@tailwind 'lib/path/file1.ext';
5 changes: 3 additions & 2 deletions packages/sv/lib/core/tests/css/common/add-at-rule/run.ts
Original file line number Diff line number Diff line change
@@ -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 });
}
3 changes: 0 additions & 3 deletions packages/sv/lib/core/tests/css/common/add-comment/input.css

This file was deleted.

4 changes: 0 additions & 4 deletions packages/sv/lib/core/tests/css/common/add-comment/output.css

This file was deleted.

5 changes: 0 additions & 5 deletions packages/sv/lib/core/tests/css/common/add-comment/run.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
@import 'lib/path/file.css';

.foo {
color: red;
}
5 changes: 3 additions & 2 deletions packages/sv/lib/core/tests/css/common/add-imports/run.ts
Original file line number Diff line number Diff line change
@@ -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'"]
});
Expand Down
1 change: 1 addition & 0 deletions packages/sv/lib/core/tests/css/common/add-rule/output.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.foo {
color: red;
}

.bar {
color: blue;
}
7 changes: 4 additions & 3 deletions packages/sv/lib/core/tests/css/common/add-rule/run.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
148 changes: 107 additions & 41 deletions packages/sv/lib/core/tooling/css/index.ts
Original file line number Diff line number Diff line change
@@ -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);
}
47 changes: 23 additions & 24 deletions packages/sv/lib/core/tooling/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,13 +18,6 @@ export {
Element as HtmlElement,
ElementType as HtmlElementType,

// css
CssAst,
Declaration,
Rule,
AtRule,
Comment,

// ast walker
Walker
};
Expand All @@ -44,10 +28,7 @@ export type {
SvelteAst,

// js
TsEstree as AstTypes,

//css
CssChildNode
TsEstree as AstTypes
};

/**
Expand Down Expand Up @@ -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(`<style>${content}</style>`);
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 (<style>) that we do not want here.
// `svelte` is unable to print an array of rules (ast.children) directly, therefore we concatenate the printed rules manually.

let result = '';

for (let i = 0; i < ast.children.length; i++) {
const child = ast.children[i];
result += sveltePrint(child).code;

if (i < ast.children.length - 1) {
const next = ast.children[i + 1];

if (child.type === 'Atrule' && next.type === 'Atrule') result += '\n';
else result += '\n\n';
}
}

return result;
}

export function parseHtml(content: string): Document {
Expand Down
Loading
Loading