Skip to content

Added style AST to SvelteStyleElement #318

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
024f9e9
Added PostCSS AST to SvelteStyleElement
marekdedic Apr 12, 2023
93ef53d
Passing file name to PostCSS parser
marekdedic Apr 15, 2023
0e13130
Added support for SCSS parsing
marekdedic Apr 16, 2023
c105390
Not parsing SASS syntax using SCSS parser
marekdedic Apr 16, 2023
8d8b70e
Emiting a warning on unknown <style> language
marekdedic Apr 16, 2023
51bec0c
Converting PostCSS AST to ESLint AST: adding the range property
marekdedic Apr 17, 2023
1e612f5
Exporting <style> contents as ESLintCompatiblePostCSSNode
marekdedic Apr 17, 2023
b2c30a4
Renaming all PostCSS node types to prevent confusion with exisiting N…
marekdedic Apr 17, 2023
414c85a
Removed unneeded type assertion
marekdedic Apr 17, 2023
4fe0c9a
Converting PostCSS AST to ESLint AST: adding the loc property
marekdedic Apr 17, 2023
572248c
Fixed lint issues by adding type assertions and checks
marekdedic Apr 17, 2023
cc8f48f
Removing file location from PostCSS AST
marekdedic Apr 17, 2023
f4d802e
Updated test fixtures
marekdedic Apr 17, 2023
98319a6
Simplified ESLintCompatiblePostCSSNode so that type inference actuall…
marekdedic Apr 17, 2023
5aa2f2b
Adding parent to PostCSS root
marekdedic Apr 17, 2023
1db5f7d
Fixed ESLintCompatiblePostCSSNode for Container subclasses
marekdedic Apr 17, 2023
9cfc436
Fixed type inference on the ESLintCompatiblePostCSSNode type
marekdedic Apr 18, 2023
20aa0c5
Re-implemented PostCSS container
marekdedic Apr 18, 2023
7a9fe6e
Removed default generic value from ESLintCompatiblePostCSSNode
marekdedic Apr 18, 2023
dc08b51
Fixed type discrimination
marekdedic Apr 18, 2023
832cbdc
Fixed Root.parent type
marekdedic Apr 18, 2023
2bf2125
Removed uneeded re-implementation of postcss Container
marekdedic Apr 18, 2023
003418f
Fixed Container.walk() callback type
marekdedic Apr 19, 2023
acd0d56
Overriden the rest of Container properties
marekdedic Apr 19, 2023
ab5b55e
Merge branch 'main' into postcss-style-parsing
marekdedic May 22, 2023
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions src/ast/html.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -66,6 +68,7 @@ export interface SvelteStyleElement extends BaseSvelteElement {
type: "SvelteStyleElement";
name: SvelteName;
startTag: SvelteStartTag;
body: ESLintCompatiblePostCSSNode<Root> | undefined;
children: [SvelteText];
endTag: SvelteEndTag | null;
parent: SvelteProgram;
Expand Down
1 change: 1 addition & 0 deletions src/ast/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
125 changes: 125 additions & 0 deletions src/ast/style.ts
Original file line number Diff line number Diff line change
@@ -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<Container<ESLintCompatiblePostCSSNode<Child>>, RedefinedProperties> & {
walk(
callback: (
node: ESLintCompatiblePostCSSNode<ChildNode>,
index: number
) => false | void
): false | undefined;
walkDecls(
propFilter: string | RegExp,
callback: (
decl: ESLintCompatiblePostCSSNode<Declaration>,
index: number
) => false | void
): false | undefined;
walkDecls(
callback: (
decl: ESLintCompatiblePostCSSNode<Declaration>,
index: number
) => false | void
): false | undefined;
walkRules(
selectorFilter: string | RegExp,
callback: (
rule: ESLintCompatiblePostCSSNode<Rule>,
index: number
) => false | void
): false | undefined;
walkRules(
callback: (
rule: ESLintCompatiblePostCSSNode<Rule>,
index: number
) => false | void
): false | undefined;
walkAtRules(
nameFilter: string | RegExp,
callback: (
atRule: ESLintCompatiblePostCSSNode<AtRule>,
index: number
) => false | void
): false | undefined;
walkAtRules(
callback: (
atRule: ESLintCompatiblePostCSSNode<AtRule>,
index: number
) => false | void
): false | undefined;
walkComments(
callback: (
comment: ESLintCompatiblePostCSSNode<Comment>,
indexed: number
) => false | void
): false | undefined;
walkComments(
callback: (
comment: ESLintCompatiblePostCSSNode<Comment>,
indexed: number
) => false | void
): false | undefined;
append(
...nodes: (
| ESLintCompatiblePostCSSNode<Node>
| ESLintCompatiblePostCSSNode<Node>[]
| ChildProps
| ChildProps[]
| string
| string[]
)[]
): ESLintCompatiblePostCSSNode<PostCSSNode>;
prepend(
...nodes: (
| ESLintCompatiblePostCSSNode<Node>
| ESLintCompatiblePostCSSNode<Node>[]
| ChildProps
| ChildProps[]
| string
| string[]
)[]
): ESLintCompatiblePostCSSNode<PostCSSNode>;
};

export type ESLintCompatiblePostCSSNode<PostCSSNode extends Node> =
// The following hack makes the `type` property work for type narrowing, see microsoft/TypeScript#53887.
PostCSSNode extends any
? Locations &
Omit<PostCSSNode, RedefinedProperties> & {
type: `SvelteStyle-${PostCSSNode["type"]}`;
} & (PostCSSNode extends Container<infer Child>
? ESLintCompatiblePostCSSContainer<PostCSSNode, Child>
: unknown) &
(PostCSSNode extends Root
? {
parent: SvelteStyleElement;
}
: {
parent: PostCSSNode["parent"];
})
: never;
83 changes: 82 additions & 1 deletion src/parser/converts/root.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as SvAST from "../svelte-ast-types";
import type {
SourceLocation,
SvelteName,
SvelteProgram,
SvelteScriptElement,
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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<Root> | undefined = postcss.parse;
switch (lang) {
case "css":
parseFn = postcss.parse;
break;
case "scss":
parseFn = SCSSparse;
break;
default:
console.warn(`Unknown <style> block language "${lang}".`);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not the right thing to do...

parseFn = undefined;
}
const contentRange = {
start: style.startTag.range[1],
end: style.endTag.range[0],
};
const styleCode = ctx.code.slice(contentRange.start, contentRange.end);
if (parseFn !== undefined) {
// The assertion here is a bit of a lie, the body only becomes `ESLintCompatiblePostCSSNode` after the convertPostCSSNodeToESLintNode function has been called on it and all its descendants.
style.body = parseFn(styleCode, {
from: ctx.parserOptions.filePath,
}) as unknown as ESLintCompatiblePostCSSNode<Root>;
convertPostCSSNodeToESLintNode<Root>(
style.body,
style.loc,
contentRange
);
// Fix Root loc
style.body.loc.start.column += style.startTag.loc.end.column;
style.body.loc.end.column -=
style.endTag.loc.end.column - style.endTag.loc.start.column;
style.body?.walk((node) =>
convertPostCSSNodeToESLintNode(node, style.loc, contentRange)
);
style.body.parent = style;
delete style.body.source?.input.file;
}
ctx.addToken("HTMLText", contentRange);
style.children = [
{
type: "SvelteText",
value: ctx.code.slice(contentRange.start, contentRange.end),
value: styleCode,
parent: style,
...ctx.getConvertLocation(contentRange),
},
Expand Down Expand Up @@ -155,6 +205,37 @@ export function convertSvelteRoot(
return ast;
}

/**
* Instruments PostCSS AST nodes to also be valid ESLint AST nodes.
*/
function convertPostCSSNodeToESLintNode<PostCSSNode extends Node>(
node: ESLintCompatiblePostCSSNode<PostCSSNode>,
styleLoc: SourceLocation,
styleRange: { start: number; end: number }
) {
node.type = `SvelteStyle-${node.type as PostCSSNode["type"]}`;
const startOffset = styleRange.start + (node.source?.start?.offset ?? 0);
const endOffset =
node.source?.end !== undefined
? styleRange.start + node.source.end.offset
: styleRange.end;
node.range = [startOffset, endOffset];
const startLine = styleLoc.start.line + (node.source?.start?.line ?? 0) - 1;
const startColumn = node.source?.start?.column ?? 1;
const endLine =
node.source?.end !== undefined
? styleLoc.start.line + node.source.end.line - 1
: styleLoc.end.line;
const endColumn =
node.source?.end !== undefined
? node.source.end.column
: styleLoc.end.column;
node.loc = {
start: { line: startLine, column: startColumn },
end: { line: endLine, column: endColumn },
};
}

/** Extract attrs */
function extractAttributes(
element: SvelteScriptElement | SvelteStyleElement,
Expand Down
2 changes: 1 addition & 1 deletion src/visitor-keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type KeyofObject<T> = { [key in keyof T]: key }[keyof T];
const svelteKeys: SvelteKeysType = {
Program: ["body"],
SvelteScriptElement: ["name", "startTag", "body", "endTag"],
SvelteStyleElement: ["name", "startTag", "children", "endTag"],
SvelteStyleElement: ["name", "startTag", "body", "children", "endTag"],
SvelteElement: ["name", "startTag", "children", "endTag"],
SvelteStartTag: ["attributes"],
SvelteEndTag: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,81 @@
}
}
},
"body": {
"type": "SvelteStyle-root",
"indexes": {},
"inputs": [
{
"css": "\n\t/* styles go here */\n",
"hasBOM": false
}
],
"lastEach": 1,
"nodes": [
{
"type": "SvelteStyle-comment",
"raws": {
"before": "\n\t",
"left": " ",
"right": " "
},
"source": {
"inputId": 0,
"start": {
"offset": 2,
"line": 2,
"column": 2
},
"end": {
"offset": 21,
"line": 2,
"column": 21
}
},
"text": "styles go here",
"range": [
49,
68
],
"loc": {
"start": {
"line": 6,
"column": 2
},
"end": {
"line": 6,
"column": 21
}
}
}
],
"raws": {
"after": "\n",
"semicolon": false
},
"source": {
"inputId": 0,
"start": {
"offset": 0,
"line": 1,
"column": 1
}
},
"range": [
47,
70
],
"loc": {
"start": {
"line": 5,
"column": 8
},
"end": {
"line": 7,
"column": 0
}
}
},
"range": [
40,
78
Expand Down
Loading