diff --git a/package.json b/package.json index 0bd97eff..60ee8ec4 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,9 @@ "dependencies": { "eslint-scope": "^7.0.0", "eslint-visitor-keys": "^3.0.0", - "espree": "^9.0.0" + "espree": "^9.0.0", + "postcss": "^8.4.21", + "postcss-scss": "^4.0.6" }, "devDependencies": { "@changesets/changelog-github": "^0.4.6", diff --git a/src/ast/html.ts b/src/ast/html.ts index 106a1e83..f52cebe3 100644 --- a/src/ast/html.ts +++ b/src/ast/html.ts @@ -1,6 +1,8 @@ import type ESTree from "estree"; +import type { Root } from "postcss"; import type { BaseNode } from "./base"; import type { Token, Comment } from "./common"; +import type { ESLintCompatiblePostCSSNode } from "./style"; export type SvelteHTMLNode = | SvelteProgram @@ -66,6 +68,7 @@ export interface SvelteStyleElement extends BaseSvelteElement { type: "SvelteStyleElement"; name: SvelteName; startTag: SvelteStartTag; + body: ESLintCompatiblePostCSSNode | undefined; children: [SvelteText]; endTag: SvelteEndTag | null; parent: SvelteProgram; diff --git a/src/ast/index.ts b/src/ast/index.ts index a271aaa3..d2aee949 100644 --- a/src/ast/index.ts +++ b/src/ast/index.ts @@ -4,5 +4,6 @@ import type { SvelteScriptNode } from "./script"; export * from "./common"; export * from "./html"; export * from "./script"; +export * from "./style"; export type SvelteNode = SvelteHTMLNode | SvelteScriptNode; diff --git a/src/ast/style.ts b/src/ast/style.ts new file mode 100644 index 00000000..afdf9176 --- /dev/null +++ b/src/ast/style.ts @@ -0,0 +1,125 @@ +import type { + AtRule, + ChildNode, + ChildProps, + Comment, + Container, + Declaration, + Node, + Root, + Rule, +} from "postcss"; +import type { Locations } from "./common"; +import type { SvelteStyleElement } from "./html"; + +type RedefinedProperties = + | "type" // Redefined for all nodes to include the "SvelteStyle-" prefix + | "parent" // Redefined for Root + | "walk" // The rest are redefined for Container + | "walkDecls" + | "walkRules" + | "walkAtRules" + | "walkComments" + | "append" + | "prepend"; + +type ESLintCompatiblePostCSSContainer< + PostCSSNode extends Node, + Child extends Node +> = Omit>, RedefinedProperties> & { + walk( + callback: ( + node: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkDecls( + propFilter: string | RegExp, + callback: ( + decl: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkDecls( + callback: ( + decl: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkRules( + selectorFilter: string | RegExp, + callback: ( + rule: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkRules( + callback: ( + rule: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkAtRules( + nameFilter: string | RegExp, + callback: ( + atRule: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkAtRules( + callback: ( + atRule: ESLintCompatiblePostCSSNode, + index: number + ) => false | void + ): false | undefined; + walkComments( + callback: ( + comment: ESLintCompatiblePostCSSNode, + indexed: number + ) => false | void + ): false | undefined; + walkComments( + callback: ( + comment: ESLintCompatiblePostCSSNode, + indexed: number + ) => false | void + ): false | undefined; + append( + ...nodes: ( + | ESLintCompatiblePostCSSNode + | ESLintCompatiblePostCSSNode[] + | ChildProps + | ChildProps[] + | string + | string[] + )[] + ): ESLintCompatiblePostCSSNode; + prepend( + ...nodes: ( + | ESLintCompatiblePostCSSNode + | ESLintCompatiblePostCSSNode[] + | ChildProps + | ChildProps[] + | string + | string[] + )[] + ): ESLintCompatiblePostCSSNode; +}; + +export type ESLintCompatiblePostCSSNode = + // The following hack makes the `type` property work for type narrowing, see microsoft/TypeScript#53887. + PostCSSNode extends any + ? Locations & + Omit & { + type: `SvelteStyle-${PostCSSNode["type"]}`; + } & (PostCSSNode extends Container + ? ESLintCompatiblePostCSSContainer + : unknown) & + (PostCSSNode extends Root + ? { + parent: SvelteStyleElement; + } + : { + parent: PostCSSNode["parent"]; + }) + : never; diff --git a/src/parser/converts/root.ts b/src/parser/converts/root.ts index 500abba6..9d17dc9f 100644 --- a/src/parser/converts/root.ts +++ b/src/parser/converts/root.ts @@ -1,5 +1,6 @@ import type * as SvAST from "../svelte-ast-types"; import type { + SourceLocation, SvelteName, SvelteProgram, SvelteScriptElement, @@ -10,6 +11,10 @@ import type { Context } from "../../context"; import { convertChildren, extractElementTags } from "./element"; import { convertAttributeTokens } from "./attr"; import type { Scope } from "eslint-scope"; +import type { Node, Parser, Root } from "postcss"; +import postcss from "postcss"; +import { parse as SCSSparse } from "postcss-scss"; +import type { ESLintCompatiblePostCSSNode } from "../../ast/style"; /** * Convert root @@ -89,6 +94,7 @@ export function convertSvelteRoot( type: "SvelteStyleElement", name: null as any, startTag: null as any, + body: undefined, children: [] as any, endTag: null, parent: ast, @@ -110,15 +116,59 @@ export function convertSvelteRoot( }); if (style.endTag && style.startTag.range[1] < style.endTag.range[0]) { + let lang = "css"; + for (const attribute of style.startTag.attributes) { + if ( + attribute.type === "SvelteAttribute" && + attribute.key.name === "lang" && + attribute.value.length > 0 && + attribute.value[0].type === "SvelteLiteral" + ) { + lang = attribute.value[0].value; + } + } + let parseFn: Parser | undefined = postcss.parse; + switch (lang) { + case "css": + parseFn = postcss.parse; + break; + case "scss": + parseFn = SCSSparse; + break; + default: + console.warn(`Unknown