Skip to content

Commit

Permalink
Add support for @import (#2498)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlos (Goodwine) <[email protected]>
  • Loading branch information
nex3 and Goodwine authored Jan 30, 2025
1 parent f4908e7 commit 2cedc62
Show file tree
Hide file tree
Showing 22 changed files with 2,021 additions and 8 deletions.
2 changes: 1 addition & 1 deletion pkg/sass-parser/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

## 0.4.10

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

## 0.4.9

Expand Down
23 changes: 23 additions & 0 deletions pkg/sass-parser/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export {
ConfiguredVariableRaws,
} from './src/configured-variable';
export {Container} from './src/container';
export {
DynamicImport,
DynamicImportObjectProps,
DynamicImportProps,
DynamicImportRaws,
} from './src/dynamic-import';
export {AnyNode, Node, NodeProps, NodeType} from './src/node';
export {RawWithValue} from './src/raw-with-value';
export {
Expand Down Expand Up @@ -64,6 +70,18 @@ export {
NumberExpressionProps,
NumberExpressionRaws,
} from './src/expression/number';
export {
ImportList,
ImportListObjectProps,
ImportListProps,
ImportListRaws,
NewImport,
} from './src/import-list';
export {
ImportRule,
ImportRuleProps,
ImportRuleRaws,
} from './src/statement/import-rule';
export {
IncludeRule,
IncludeRuleProps,
Expand Down Expand Up @@ -172,6 +190,11 @@ export {
WhileRuleProps,
WhileRuleRaws,
} from './src/statement/while-rule';
export {
StaticImport,
StaticImportProps,
StaticImportRaws,
} from './src/static-import';

/** Options that can be passed to the Sass parsers to control their behavior. */
export type SassParserOptions = Pick<postcss.ProcessOptions, 'from' | 'map'>;
Expand Down
6 changes: 3 additions & 3 deletions pkg/sass-parser/lib/src/__snapshots__/argument.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a argument toJSON with a name 1`] = `
exports[`an argument toJSON with a name 1`] = `
{
"inputs": [
{
Expand All @@ -17,7 +17,7 @@ exports[`a argument toJSON with a name 1`] = `
}
`;
exports[`a argument toJSON with no name 1`] = `
exports[`an argument toJSON with no name 1`] = `
{
"inputs": [
{
Expand All @@ -33,7 +33,7 @@ exports[`a argument toJSON with no name 1`] = `
}
`;
exports[`a argument toJSON with rest 1`] = `
exports[`an argument toJSON with rest 1`] = `
{
"inputs": [
{
Expand Down
17 changes: 17 additions & 0 deletions pkg/sass-parser/lib/src/__snapshots__/dynamic-import.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a dynamic import toJSON 1`] = `
{
"inputs": [
{
"css": "@import "foo"",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "dynamic-import",
"source": <1:9-1:14 in 0>,
"url": "foo",
}
`;
20 changes: 20 additions & 0 deletions pkg/sass-parser/lib/src/__snapshots__/import-list.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`an import list toJSON 1`] = `
{
"inputs": [
{
"css": "@import "foo", "bar.css"",
"hasBOM": false,
"id": "<input css _____>",
},
],
"nodes": [
<"foo">,
<"bar.css">,
],
"raws": {},
"sassType": "import-list",
"source": <1:1-1:25 in 0>,
}
`;
34 changes: 34 additions & 0 deletions pkg/sass-parser/lib/src/__snapshots__/static-import.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`a static import toJSON with modifiers 1`] = `
{
"inputs": [
{
"css": "@import "foo.css" screen",
"hasBOM": false,
"id": "<input css _____>",
},
],
"modifiers": <screen>,
"raws": {},
"sassType": "static-import",
"source": <1:9-1:25 in 0>,
"staticUrl": <"foo.css">,
}
`;
exports[`a static import toJSON without modifiers 1`] = `
{
"inputs": [
{
"css": "@import "foo.css"",
"hasBOM": false,
"id": "<input css _____>",
},
],
"raws": {},
"sassType": "static-import",
"source": <1:9-1:18 in 0>,
"staticUrl": <"foo.css">,
}
`;
2 changes: 1 addition & 1 deletion pkg/sass-parser/lib/src/argument-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class ArgumentList
this.append({value: convertExpression(inner.keywordRest), rest: true});
}
}
if (this._nodes === undefined) this._nodes = [];
this._nodes ??= [];
}

clone(overrides?: Partial<ArgumentListObjectProps>): this {
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass-parser/lib/src/argument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
scss,
} from '..';

describe('a argument', () => {
describe('an argument', () => {
let node: Argument;
beforeEach(
() =>
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass-parser/lib/src/argument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface ArgumentRaws {

/**
* The space symbols between the end of the argument value and the comma
* afterwards. Always empty for a argument that doesn't have a trailing comma.
* afterwards. Always empty for an argument that doesn't have a trailing comma.
*/
after?: string;
}
Expand Down
155 changes: 155 additions & 0 deletions pkg/sass-parser/lib/src/dynamic-import.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2025 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import {DynamicImport, ImportList, ImportRule, sass, scss} from '..';

describe('a dynamic import', () => {
let node: DynamicImport;

function describeNode(
description: string,
create: () => DynamicImport,
): void {
describe(description, () => {
beforeEach(() => (node = create()));

it('has a sassType', () =>
expect(node.sassType.toString()).toBe('dynamic-import'));

it('has a url', () => expect(node.url).toBe('foo'));
});
}

describeNode(
'parsed as SCSS',
() =>
(scss.parse('@import "foo"').nodes[0] as ImportRule).imports
.nodes[0] as DynamicImport,
);

describeNode(
'parsed as Sass',
() =>
(sass.parse('@import "foo"').nodes[0] as ImportRule).imports
.nodes[0] as DynamicImport,
);

describe('constructed manually', () => {
describeNode('with a string', () => new DynamicImport('foo'));

describeNode('with an object', () => new DynamicImport({url: 'foo'}));
});

describe('constructed from properties', () => {
describeNode(
'with a string',
() => new ImportList({nodes: ['foo']}).nodes[0] as DynamicImport,
);

describeNode(
'with an object',
() => new ImportList({nodes: [{url: 'foo'}]}).nodes[0] as DynamicImport,
);
});

describe('stringifies', () => {
describe('to SCSS', () => {
describe('with default raws', () => {
it('with a simple URL', () =>
expect(new DynamicImport('foo').toString()).toBe('"foo"'));

it('with a URL that needs escaping', () =>
expect(new DynamicImport('\\').toString()).toBe('"\\\\"'));
});

// raws.before is only used as part of a ImportList
it('ignores before', () =>
expect(
new DynamicImport({
url: 'foo',
raws: {before: '/**/'},
}).toString(),
).toBe('"foo"'));

// raws.after is only used as part of a ImportList
it('ignores after', () =>
expect(
new DynamicImport({
url: 'foo',
raws: {after: '/**/'},
}).toString(),
).toBe('"foo"'));

it('with matching url', () =>
expect(
new DynamicImport({
url: 'foo',
raws: {url: {raw: '"f\\6fo"', value: 'foo'}},
}).toString(),
).toBe('"f\\6fo"'));

it('with non-matching url', () =>
expect(
new DynamicImport({
url: 'foo',
raws: {url: {raw: '"f\\41o"', value: 'fao'}},
}).toString(),
).toBe('"foo"'));
});
});

describe('clone()', () => {
let original: DynamicImport;
beforeEach(() => {
original = (scss.parse('@import "foo"').nodes[0] as ImportRule).imports
.nodes[0] as DynamicImport;
// TODO: remove this once raws are properly parsed.
original.raws.before = '/**/';
});

describe('with no overrides', () => {
let clone: DynamicImport;
beforeEach(() => void (clone = original.clone()));

describe('has the same properties:', () => {
it('url', () => expect(clone.url).toBe('foo'));
});

describe('creates a new', () => {
it('self', () => expect(clone).not.toBe(original));

for (const attr of ['raws'] as const) {
it(attr, () => expect(clone[attr]).not.toBe(original[attr]));
}
});
});

describe('overrides', () => {
describe('raws', () => {
it('defined', () =>
expect(original.clone({raws: {after: ' '}}).raws).toEqual({
after: ' ',
}));

it('undefined', () =>
expect(original.clone({raws: undefined}).raws).toEqual({
before: '/**/',
}));
});

describe('url', () => {
it('defined', () =>
expect(original.clone({url: 'bar'}).url).toBe('bar'));

it('undefined', () =>
expect(original.clone({url: undefined}).url).toBe('foo'));
});
});
});

it('toJSON', () =>
expect(
(scss.parse('@import "foo"').nodes[0] as ImportRule).imports.nodes[0],
).toMatchSnapshot());
});
Loading

0 comments on commit 2cedc62

Please sign in to comment.