Skip to content

Commit 6123b22

Browse files
committed
Add support for @debug rules
1 parent e1a8060 commit 6123b22

File tree

10 files changed

+400
-5
lines changed

10 files changed

+400
-5
lines changed

pkg/sass-parser/README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,3 +246,14 @@ const sassParser = require('sass-parser');
246246
const root = new sassParser.Root();
247247
root.append('content: "hello, world!"');
248248
```
249+
250+
### Known Incompatibilities
251+
252+
There are a few cases where an operation that's valid in PostCSS won't work with
253+
`sass-parser`:
254+
255+
* Trying to convert a Sass-specific at-rule like `@if` or `@mixin` into a
256+
different at-rule by changing its name is not supported.
257+
258+
* Trying to add child nodes to a Sass statement that doesn't support children
259+
like `@use` or `@error` is not supported.

pkg/sass-parser/lib/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ export {
3333
InterpolationRaws,
3434
NewNodeForInterpolation,
3535
} from './src/interpolation';
36+
export {
37+
DebugRule,
38+
DebugRuleProps,
39+
DebugRuleRaws,
40+
} from './src/statement/debug-rule';
3641
export {
3742
GenericAtRule,
3843
GenericAtRuleProps,

pkg/sass-parser/lib/src/sass-internal.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ declare namespace SassInternal {
7676
readonly value?: Interpolation;
7777
}
7878

79+
class DebugRule extends Statement {
80+
readonly expression: Expression;
81+
}
82+
7983
class Stylesheet extends ParentStatement<Statement[]> {}
8084

8185
class StyleRule extends ParentStatement<Statement[]> {
@@ -113,6 +117,7 @@ export type ParentStatement<T extends Statement[] | null> =
113117
SassInternal.ParentStatement<T>;
114118
export type AtRootRule = SassInternal.AtRootRule;
115119
export type AtRule = SassInternal.AtRule;
120+
export type DebugRule = SassInternal.DebugRule;
116121
export type Stylesheet = SassInternal.Stylesheet;
117122
export type StyleRule = SassInternal.StyleRule;
118123
export type Interpolation = SassInternal.Interpolation;
@@ -123,6 +128,7 @@ export type StringExpression = SassInternal.StringExpression;
123128
export interface StatementVisitorObject<T> {
124129
visitAtRootRule(node: AtRootRule): T;
125130
visitAtRule(node: AtRule): T;
131+
visitDebugRule(node: DebugRule): T;
126132
visitStyleRule(node: StyleRule): T;
127133
}
128134

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a @debug rule toJSON 1`] = `
4+
{
5+
"debugExpression": <foo>,
6+
"inputs": [
7+
{
8+
"css": "@debug foo",
9+
"hasBOM": false,
10+
"id": "<input css _____>",
11+
},
12+
],
13+
"name": "debug",
14+
"params": "foo",
15+
"raws": {},
16+
"sassType": "debug-rule",
17+
"source": <1:1-1:11 in 0>,
18+
"type": "atrule",
19+
}
20+
`;

pkg/sass-parser/lib/src/statement/at-rule-internal.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,6 @@ import {AtRule, ChildNode, Comment, Declaration, NewNode} from '.';
1515
* @hidden
1616
*/
1717
export class _AtRule<Props> extends postcss.AtRule {
18-
declare nodes: ChildNode[];
19-
2018
// Override the PostCSS container types to constrain them to Sass types only.
2119
// Unfortunately, there's no way to abstract this out, because anything
2220
// mixin-like returns an intersection type which doesn't actually override
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
// Copyright 2024 Google Inc. Use of this source code is governed by an
2+
// MIT-style license that can be found in the LICENSE file or at
3+
// https://opensource.org/licenses/MIT.
4+
5+
import {DebugRule, StringExpression, sass, scss} from '../..';
6+
import * as utils from '../../../test/utils';
7+
8+
describe('a @debug rule', () => {
9+
let node: DebugRule;
10+
function describeNode(description: string, create: () => DebugRule): void {
11+
describe(description, () => {
12+
beforeEach(() => void (node = create()));
13+
14+
it('has a name', () => expect(node.name.toString()).toBe('debug'));
15+
16+
it('has an expression', () =>
17+
expect(node).toHaveStringExpression('debugExpression', 'foo'));
18+
19+
it('has matching params', () => expect(node.params).toBe('foo'));
20+
21+
it('has undefined nodes', () => expect(node.nodes).toBeUndefined());
22+
});
23+
}
24+
25+
describeNode(
26+
'parsed as SCSS',
27+
() => scss.parse('@debug foo').nodes[0] as DebugRule
28+
);
29+
30+
describeNode(
31+
'parsed as Sass',
32+
() => sass.parse('@debug foo').nodes[0] as DebugRule
33+
);
34+
35+
describeNode(
36+
'constructed manually',
37+
() =>
38+
new DebugRule({
39+
debugExpression: {text: 'foo'},
40+
})
41+
);
42+
43+
describeNode('constructed from ChildProps', () =>
44+
utils.fromChildProps({
45+
debugExpression: {text: 'foo'},
46+
})
47+
);
48+
49+
it('throws an error when assigned a new name', () =>
50+
expect(
51+
() =>
52+
(new DebugRule({
53+
debugExpression: {text: 'foo'},
54+
}).name = 'bar')
55+
).toThrow());
56+
57+
describe('assigned a new expression', () => {
58+
beforeEach(() => {
59+
node = scss.parse('@debug foo').nodes[0] as DebugRule;
60+
});
61+
62+
it('sets an empty string expression as undefined params', () => {
63+
node.params = undefined;
64+
expect(node.params).toBe('');
65+
expect(node).toHaveStringExpression('debugExpression', '');
66+
});
67+
68+
it('sets an empty string expression as empty string params', () => {
69+
node.params = '';
70+
expect(node.params).toBe('');
71+
expect(node).toHaveStringExpression('debugExpression', '');
72+
});
73+
74+
it("removes the old expression's parent", () => {
75+
const oldExpression = node.debugExpression;
76+
node.debugExpression = {text: 'bar'};
77+
expect(oldExpression.parent).toBeUndefined();
78+
});
79+
80+
it("assigns the new expression's parent", () => {
81+
const expression = new StringExpression({text: 'bar'});
82+
node.debugExpression = expression;
83+
expect(expression.parent).toBe(node);
84+
});
85+
86+
it('assigns the expression explicitly', () => {
87+
const expression = new StringExpression({text: 'bar'});
88+
node.debugExpression = expression;
89+
expect(node.debugExpression).toBe(expression);
90+
});
91+
92+
it('assigns the expression as ExpressionProps', () => {
93+
node.debugExpression = {text: 'bar'};
94+
expect(node).toHaveStringExpression('debugExpression', 'bar');
95+
});
96+
97+
it('assigns the expression as params', () => {
98+
node.params = 'bar';
99+
expect(node).toHaveStringExpression('debugExpression', 'bar');
100+
});
101+
});
102+
103+
describe('stringifies', () => {
104+
describe('to SCSS', () => {
105+
it('with default raws', () =>
106+
expect(
107+
new DebugRule({
108+
debugExpression: {text: 'foo'},
109+
}).toString()
110+
).toBe('@debug foo;'));
111+
112+
it('with afterName', () =>
113+
expect(
114+
new DebugRule({
115+
debugExpression: {text: 'foo'},
116+
raws: {afterName: '/**/'},
117+
}).toString()
118+
).toBe('@debug/**/foo;'));
119+
120+
it('with between', () =>
121+
expect(
122+
new DebugRule({
123+
debugExpression: {text: 'foo'},
124+
raws: {between: '/**/'},
125+
}).toString()
126+
).toBe('@debug foo/**/;'));
127+
});
128+
});
129+
130+
describe('clone', () => {
131+
let original: DebugRule;
132+
beforeEach(() => {
133+
original = scss.parse('@debug foo').nodes[0] as DebugRule;
134+
// TODO: remove this once raws are properly parsed
135+
original.raws.between = ' ';
136+
});
137+
138+
describe('with no overrides', () => {
139+
let clone: DebugRule;
140+
beforeEach(() => void (clone = original.clone()));
141+
142+
describe('has the same properties:', () => {
143+
it('params', () => expect(clone.params).toBe('foo'));
144+
145+
it('debugExpression', () =>
146+
expect(clone).toHaveStringExpression('debugExpression', 'foo'));
147+
148+
it('raws', () => expect(clone.raws).toEqual({between: ' '}));
149+
150+
it('source', () => expect(clone.source).toBe(original.source));
151+
});
152+
153+
describe('creates a new', () => {
154+
it('self', () => expect(clone).not.toBe(original));
155+
156+
for (const attr of ['debugExpression', 'raws'] as const) {
157+
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
158+
}
159+
});
160+
});
161+
162+
describe('overrides', () => {
163+
describe('raws', () => {
164+
it('defined', () =>
165+
expect(original.clone({raws: {afterName: ' '}}).raws).toEqual({
166+
afterName: ' ',
167+
}));
168+
169+
it('undefined', () =>
170+
expect(original.clone({raws: undefined}).raws).toEqual({
171+
between: ' ',
172+
}));
173+
});
174+
175+
describe('debugExpression', () => {
176+
describe('defined', () => {
177+
let clone: DebugRule;
178+
beforeEach(() => {
179+
clone = original.clone({debugExpression: {text: 'bar'}});
180+
});
181+
182+
it('changes params', () => expect(clone.params).toBe('bar'));
183+
184+
it('changes debugExpression', () =>
185+
expect(clone).toHaveStringExpression('debugExpression', 'bar'));
186+
});
187+
188+
describe('undefined', () => {
189+
let clone: DebugRule;
190+
beforeEach(() => {
191+
clone = original.clone({debugExpression: undefined});
192+
});
193+
194+
it('preserves params', () => expect(clone.params).toBe('foo'));
195+
196+
it('preserves debugExpression', () =>
197+
expect(clone).toHaveStringExpression('debugExpression', 'foo'));
198+
});
199+
});
200+
});
201+
});
202+
203+
it('toJSON', () =>
204+
expect(scss.parse('@debug foo').nodes[0]).toMatchSnapshot());
205+
});

0 commit comments

Comments
 (0)