Skip to content
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

Track dependencies through meta.load-css() with --watch #1877

Merged
merged 1 commit into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
* Add the relative length units from CSS Values 4 and CSS Contain 3 as known
units to validate bad computation in `calc`.

### Command Line Interface

* The `--watch` flag will now track loads through calls to `meta.load-css()` as
long as their URLs are literal strings without any interpolation.

## 1.57.1

* No user-visible changes.
Expand Down
10 changes: 5 additions & 5 deletions lib/src/stylesheet_graph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,17 +111,17 @@ class StylesheetGraph {
/// Returns two maps from non-canonicalized imported URLs in [stylesheet] to
/// nodes, which appears within [baseUrl] imported by [baseImporter].
///
/// The first map contains stylesheets depended on via `@use` and `@forward`
/// while the second map contains those depended on via `@import`.
/// The first map contains stylesheets depended on via module loads while the
/// second map contains those depended on via `@import`.
Tuple2<Map<Uri, StylesheetNode?>, Map<Uri, StylesheetNode?>> _upstreamNodes(
Stylesheet stylesheet, Importer baseImporter, Uri baseUrl) {
var active = {baseUrl};
var tuple = findDependencies(stylesheet);
var dependencies = findDependencies(stylesheet);
return Tuple2({
for (var url in tuple.item1)
for (var url in dependencies.modules)
url: _nodeFor(url, baseImporter, baseUrl, active)
}, {
for (var url in tuple.item2)
for (var url in dependencies.imports)
url: _nodeFor(url, baseImporter, baseUrl, active, forImport: true)
});
}
Expand Down
85 changes: 70 additions & 15 deletions lib/src/visitor/find_dependencies.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,38 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.

import 'package:tuple/tuple.dart';
import 'package:collection/collection.dart';

import '../ast/sass.dart';
import 'recursive_statement.dart';

/// Returns two lists of dependencies for [stylesheet].
///
/// The first is a list of URLs from all `@use` and `@forward` rules in
/// [stylesheet] (excluding built-in modules). The second is a list of all
/// imports in [stylesheet].
/// Returns [stylesheet]'s statically-declared dependencies.
///
/// {@category Dependencies}
Tuple2<List<Uri>, List<Uri>> findDependencies(Stylesheet stylesheet) =>
DependencyReport findDependencies(Stylesheet stylesheet) =>
_FindDependenciesVisitor().run(stylesheet);

/// A visitor that traverses a stylesheet and records, all `@import`, `@use`,
/// and `@forward` rules (excluding built-in modules) it contains.
/// A visitor that traverses a stylesheet and records all its dependencies on
/// other stylesheets.
class _FindDependenciesVisitor with RecursiveStatementVisitor {
final _usesAndForwards = <Uri>[];
final _imports = <Uri>[];
final _uses = <Uri>{};
final _forwards = <Uri>{};
final _metaLoadCss = <Uri>{};
final _imports = <Uri>{};

/// The namespaces under which `sass:meta` has been `@use`d in this stylesheet.
///
/// If this contains `null`, it means `sass:meta` was loaded without a
/// namespace.
final _metaNamespaces = <String?>{};

Tuple2<List<Uri>, List<Uri>> run(Stylesheet stylesheet) {
DependencyReport run(Stylesheet stylesheet) {
visitStylesheet(stylesheet);
return Tuple2(_usesAndForwards, _imports);
return DependencyReport._(
uses: UnmodifiableSetView(_uses),
forwards: UnmodifiableSetView(_forwards),
metaLoadCss: UnmodifiableSetView(_metaLoadCss),
imports: UnmodifiableSetView(_imports));
}

// These can never contain imports.
Expand All @@ -38,16 +46,63 @@ class _FindDependenciesVisitor with RecursiveStatementVisitor {
void visitSupportsCondition(SupportsCondition condition) {}

void visitUseRule(UseRule node) {
if (node.url.scheme != 'sass') _usesAndForwards.add(node.url);
if (node.url.scheme != 'sass') {
_uses.add(node.url);
} else if (node.url.toString() == 'sass:meta') {
_metaNamespaces.add(node.namespace);
}
}

void visitForwardRule(ForwardRule node) {
if (node.url.scheme != 'sass') _usesAndForwards.add(node.url);
if (node.url.scheme != 'sass') _forwards.add(node.url);
}

void visitImportRule(ImportRule node) {
for (var import in node.imports) {
if (import is DynamicImport) _imports.add(import.url);
}
}

void visitIncludeRule(IncludeRule node) {
if (node.name != 'load-css') return;
if (!_metaNamespaces.contains(node.namespace)) return;
if (node.arguments.positional.isEmpty) return;
var argument = node.arguments.positional.first;
if (argument is! StringExpression) return;
var url = argument.text.asPlain;
try {
if (url != null) _metaLoadCss.add(Uri.parse(url));
} on FormatException {
// Ignore invalid URLs.
}
}
}

/// A struct of different types of dependencies a Sass stylesheet can contain.
class DependencyReport {
/// An unmodifiable set of all `@use`d URLs in the stylesheet (exluding
/// built-in modules).
final Set<Uri> uses;

/// An unmodifiable set of all `@forward`ed URLs in the stylesheet (excluding
/// built-in modules).
final Set<Uri> forwards;

/// An unmodifiable set of all URLs loaded by `meta.load-css()` calls with
/// static string arguments outside of mixins.
final Set<Uri> metaLoadCss;

/// An unmodifiable set of all dynamically `@import`ed URLs in the
/// stylesheet.
final Set<Uri> imports;

/// An unmodifiable set of all URLs in [uses], [forwards], and [metaLoadCss].
Set<Uri> get modules => UnionSet({uses, forwards, metaLoadCss});

/// An unmodifiable set of all URLs in [uses], [forwards], [metaLoadCss], and
/// [imports].
Set<Uri> get all => UnionSet({uses, forwards, metaLoadCss, imports});

DependencyReport._(
{required this.uses, required this.forwards, required this.metaLoadCss, required this.imports});
}
7 changes: 7 additions & 0 deletions pkg/sass_api/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## 5.0.0

* **Breaking change:** Instead of a `Tuple`, `findDependencies()` now returns a
`DependencyReport` object with named fields. This provides finer-grained
access to import URLs, as well as information about `meta.load-css()` calls
with non-interpolated string literal arguments.

## 4.2.2

* No user-visible changes.
Expand Down
2 changes: 1 addition & 1 deletion pkg/sass_api/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: sass_api
# Note: Every time we add a new Sass AST node, we need to bump the *major*
# version because it's a breaking change for anyone who's implementing the
# visitor interface(s).
version: 4.2.2
version: 5.0.0
description: Additional APIs for Dart Sass.
homepage: https://github.com/sass/dart-sass

Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: sass
version: 1.58.0-dev
version: 1.58.0
description: A Sass implementation in Dart.
homepage: https://github.com/sass/dart-sass

Expand Down
94 changes: 94 additions & 0 deletions test/cli/shared/watch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,100 @@ void sharedTests(Future<TestProcess> runSass(Iterable<String> arguments)) {
.validate();
});

group("through meta.load-css", () {
test("with the default namespace", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", """
@use 'sass:meta';
@include meta.load-css('other');
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});

test("with a custom namespace", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", """
@use 'sass:meta' as m;
@include m.load-css('other');
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});

test("with no namespace", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", """
@use 'sass:meta' as *;
@include load-css('other');
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});

test(r"with $with", () async {
await d.file("_other.scss", "a {b: c}").create();
await d.file("test.scss", r"""
@use 'sass:meta';
@include meta.load-css('other', $with: ());
""").create();

var sass = await watch(["test.scss:out.css"]);
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await expectLater(sass.stdout, _watchingForChanges);
await tickIfPoll();

await d.file("_other.scss", "x {y: z}").create();
await expectLater(
sass.stdout, emits('Compiled test.scss to out.css.'));
await sass.kill();

await d
.file("out.css", equalsIgnoringWhitespace("x { y: z; }"))
.validate();
});
});

// Regression test for #550
test("with an error that's later fixed", () async {
await d.file("_other.scss", "a {b: c}").create();
Expand Down