Skip to content

Commit 2cedc62

Browse files
nex3Goodwine
andauthored
Add support for @import (#2498)
Co-authored-by: Carlos (Goodwine) <[email protected]>
1 parent f4908e7 commit 2cedc62

22 files changed

+2021
-8
lines changed

pkg/sass-parser/CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
## 0.4.10
1414

15-
* No user-visible changes.
15+
* Add support for parsing the `@import` rule.
1616

1717
## 0.4.9
1818

pkg/sass-parser/lib/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ export {
3535
ConfiguredVariableRaws,
3636
} from './src/configured-variable';
3737
export {Container} from './src/container';
38+
export {
39+
DynamicImport,
40+
DynamicImportObjectProps,
41+
DynamicImportProps,
42+
DynamicImportRaws,
43+
} from './src/dynamic-import';
3844
export {AnyNode, Node, NodeProps, NodeType} from './src/node';
3945
export {RawWithValue} from './src/raw-with-value';
4046
export {
@@ -64,6 +70,18 @@ export {
6470
NumberExpressionProps,
6571
NumberExpressionRaws,
6672
} from './src/expression/number';
73+
export {
74+
ImportList,
75+
ImportListObjectProps,
76+
ImportListProps,
77+
ImportListRaws,
78+
NewImport,
79+
} from './src/import-list';
80+
export {
81+
ImportRule,
82+
ImportRuleProps,
83+
ImportRuleRaws,
84+
} from './src/statement/import-rule';
6785
export {
6886
IncludeRule,
6987
IncludeRuleProps,
@@ -172,6 +190,11 @@ export {
172190
WhileRuleProps,
173191
WhileRuleRaws,
174192
} from './src/statement/while-rule';
193+
export {
194+
StaticImport,
195+
StaticImportProps,
196+
StaticImportRaws,
197+
} from './src/static-import';
175198

176199
/** Options that can be passed to the Sass parsers to control their behavior. */
177200
export type SassParserOptions = Pick<postcss.ProcessOptions, 'from' | 'map'>;

pkg/sass-parser/lib/src/__snapshots__/argument.test.ts.snap

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3-
exports[`a argument toJSON with a name 1`] = `
3+
exports[`an argument toJSON with a name 1`] = `
44
{
55
"inputs": [
66
{
@@ -17,7 +17,7 @@ exports[`a argument toJSON with a name 1`] = `
1717
}
1818
`;
1919
20-
exports[`a argument toJSON with no name 1`] = `
20+
exports[`an argument toJSON with no name 1`] = `
2121
{
2222
"inputs": [
2323
{
@@ -33,7 +33,7 @@ exports[`a argument toJSON with no name 1`] = `
3333
}
3434
`;
3535
36-
exports[`a argument toJSON with rest 1`] = `
36+
exports[`an argument toJSON with rest 1`] = `
3737
{
3838
"inputs": [
3939
{
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a dynamic import toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@import "foo"",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"raws": {},
13+
"sassType": "dynamic-import",
14+
"source": <1:9-1:14 in 0>,
15+
"url": "foo",
16+
}
17+
`;
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[`an import list toJSON 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@import "foo", "bar.css"",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"nodes": [
13+
<"foo">,
14+
<"bar.css">,
15+
],
16+
"raws": {},
17+
"sassType": "import-list",
18+
"source": <1:1-1:25 in 0>,
19+
}
20+
`;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`a static import toJSON with modifiers 1`] = `
4+
{
5+
"inputs": [
6+
{
7+
"css": "@import "foo.css" screen",
8+
"hasBOM": false,
9+
"id": "<input css _____>",
10+
},
11+
],
12+
"modifiers": <screen>,
13+
"raws": {},
14+
"sassType": "static-import",
15+
"source": <1:9-1:25 in 0>,
16+
"staticUrl": <"foo.css">,
17+
}
18+
`;
19+
20+
exports[`a static import toJSON without modifiers 1`] = `
21+
{
22+
"inputs": [
23+
{
24+
"css": "@import "foo.css"",
25+
"hasBOM": false,
26+
"id": "<input css _____>",
27+
},
28+
],
29+
"raws": {},
30+
"sassType": "static-import",
31+
"source": <1:9-1:18 in 0>,
32+
"staticUrl": <"foo.css">,
33+
}
34+
`;

pkg/sass-parser/lib/src/argument-list.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class ArgumentList
119119
this.append({value: convertExpression(inner.keywordRest), rest: true});
120120
}
121121
}
122-
if (this._nodes === undefined) this._nodes = [];
122+
this._nodes ??= [];
123123
}
124124

125125
clone(overrides?: Partial<ArgumentListObjectProps>): this {

pkg/sass-parser/lib/src/argument.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
scss,
1212
} from '..';
1313

14-
describe('a argument', () => {
14+
describe('an argument', () => {
1515
let node: Argument;
1616
beforeEach(
1717
() =>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ export interface ArgumentRaws {
4747

4848
/**
4949
* The space symbols between the end of the argument value and the comma
50-
* afterwards. Always empty for a argument that doesn't have a trailing comma.
50+
* afterwards. Always empty for an argument that doesn't have a trailing comma.
5151
*/
5252
after?: string;
5353
}
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
// Copyright 2025 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 {DynamicImport, ImportList, ImportRule, sass, scss} from '..';
6+
7+
describe('a dynamic import', () => {
8+
let node: DynamicImport;
9+
10+
function describeNode(
11+
description: string,
12+
create: () => DynamicImport,
13+
): void {
14+
describe(description, () => {
15+
beforeEach(() => (node = create()));
16+
17+
it('has a sassType', () =>
18+
expect(node.sassType.toString()).toBe('dynamic-import'));
19+
20+
it('has a url', () => expect(node.url).toBe('foo'));
21+
});
22+
}
23+
24+
describeNode(
25+
'parsed as SCSS',
26+
() =>
27+
(scss.parse('@import "foo"').nodes[0] as ImportRule).imports
28+
.nodes[0] as DynamicImport,
29+
);
30+
31+
describeNode(
32+
'parsed as Sass',
33+
() =>
34+
(sass.parse('@import "foo"').nodes[0] as ImportRule).imports
35+
.nodes[0] as DynamicImport,
36+
);
37+
38+
describe('constructed manually', () => {
39+
describeNode('with a string', () => new DynamicImport('foo'));
40+
41+
describeNode('with an object', () => new DynamicImport({url: 'foo'}));
42+
});
43+
44+
describe('constructed from properties', () => {
45+
describeNode(
46+
'with a string',
47+
() => new ImportList({nodes: ['foo']}).nodes[0] as DynamicImport,
48+
);
49+
50+
describeNode(
51+
'with an object',
52+
() => new ImportList({nodes: [{url: 'foo'}]}).nodes[0] as DynamicImport,
53+
);
54+
});
55+
56+
describe('stringifies', () => {
57+
describe('to SCSS', () => {
58+
describe('with default raws', () => {
59+
it('with a simple URL', () =>
60+
expect(new DynamicImport('foo').toString()).toBe('"foo"'));
61+
62+
it('with a URL that needs escaping', () =>
63+
expect(new DynamicImport('\\').toString()).toBe('"\\\\"'));
64+
});
65+
66+
// raws.before is only used as part of a ImportList
67+
it('ignores before', () =>
68+
expect(
69+
new DynamicImport({
70+
url: 'foo',
71+
raws: {before: '/**/'},
72+
}).toString(),
73+
).toBe('"foo"'));
74+
75+
// raws.after is only used as part of a ImportList
76+
it('ignores after', () =>
77+
expect(
78+
new DynamicImport({
79+
url: 'foo',
80+
raws: {after: '/**/'},
81+
}).toString(),
82+
).toBe('"foo"'));
83+
84+
it('with matching url', () =>
85+
expect(
86+
new DynamicImport({
87+
url: 'foo',
88+
raws: {url: {raw: '"f\\6fo"', value: 'foo'}},
89+
}).toString(),
90+
).toBe('"f\\6fo"'));
91+
92+
it('with non-matching url', () =>
93+
expect(
94+
new DynamicImport({
95+
url: 'foo',
96+
raws: {url: {raw: '"f\\41o"', value: 'fao'}},
97+
}).toString(),
98+
).toBe('"foo"'));
99+
});
100+
});
101+
102+
describe('clone()', () => {
103+
let original: DynamicImport;
104+
beforeEach(() => {
105+
original = (scss.parse('@import "foo"').nodes[0] as ImportRule).imports
106+
.nodes[0] as DynamicImport;
107+
// TODO: remove this once raws are properly parsed.
108+
original.raws.before = '/**/';
109+
});
110+
111+
describe('with no overrides', () => {
112+
let clone: DynamicImport;
113+
beforeEach(() => void (clone = original.clone()));
114+
115+
describe('has the same properties:', () => {
116+
it('url', () => expect(clone.url).toBe('foo'));
117+
});
118+
119+
describe('creates a new', () => {
120+
it('self', () => expect(clone).not.toBe(original));
121+
122+
for (const attr of ['raws'] as const) {
123+
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
124+
}
125+
});
126+
});
127+
128+
describe('overrides', () => {
129+
describe('raws', () => {
130+
it('defined', () =>
131+
expect(original.clone({raws: {after: ' '}}).raws).toEqual({
132+
after: ' ',
133+
}));
134+
135+
it('undefined', () =>
136+
expect(original.clone({raws: undefined}).raws).toEqual({
137+
before: '/**/',
138+
}));
139+
});
140+
141+
describe('url', () => {
142+
it('defined', () =>
143+
expect(original.clone({url: 'bar'}).url).toBe('bar'));
144+
145+
it('undefined', () =>
146+
expect(original.clone({url: undefined}).url).toBe('foo'));
147+
});
148+
});
149+
});
150+
151+
it('toJSON', () =>
152+
expect(
153+
(scss.parse('@import "foo"').nodes[0] as ImportRule).imports.nodes[0],
154+
).toMatchSnapshot());
155+
});

0 commit comments

Comments
 (0)