From a416e8edadab25318324f2cad88955dcaf4560f2 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 31 Jan 2023 16:25:59 -0800 Subject: [PATCH] Track dependencies through `meta.load-css()` with `--watch` Closes #1808 --- CHANGELOG.md | 5 ++ lib/src/stylesheet_graph.dart | 10 +-- lib/src/visitor/find_dependencies.dart | 85 +++++++++++++++++++---- pkg/sass_api/CHANGELOG.md | 7 ++ pkg/sass_api/pubspec.yaml | 2 +- pubspec.yaml | 2 +- test/cli/shared/watch.dart | 94 ++++++++++++++++++++++++++ 7 files changed, 183 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b371fe85d..b64a1a8de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/lib/src/stylesheet_graph.dart b/lib/src/stylesheet_graph.dart index 0b19cb76e..1cd1823a0 100644 --- a/lib/src/stylesheet_graph.dart +++ b/lib/src/stylesheet_graph.dart @@ -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> _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) }); } diff --git a/lib/src/visitor/find_dependencies.dart b/lib/src/visitor/find_dependencies.dart index 362157a0c..010ac2714 100644 --- a/lib/src/visitor/find_dependencies.dart +++ b/lib/src/visitor/find_dependencies.dart @@ -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> 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 = []; - final _imports = []; + final _uses = {}; + final _forwards = {}; + final _metaLoadCss = {}; + final _imports = {}; + + /// 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 = {}; - Tuple2, List> 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. @@ -38,11 +46,15 @@ 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) { @@ -50,4 +62,47 @@ class _FindDependenciesVisitor with RecursiveStatementVisitor { 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 uses; + + /// An unmodifiable set of all `@forward`ed URLs in the stylesheet (excluding + /// built-in modules). + final Set forwards; + + /// An unmodifiable set of all URLs loaded by `meta.load-css()` calls with + /// static string arguments outside of mixins. + final Set metaLoadCss; + + /// An unmodifiable set of all dynamically `@import`ed URLs in the + /// stylesheet. + final Set imports; + + /// An unmodifiable set of all URLs in [uses], [forwards], and [metaLoadCss]. + Set get modules => UnionSet({uses, forwards, metaLoadCss}); + + /// An unmodifiable set of all URLs in [uses], [forwards], [metaLoadCss], and + /// [imports]. + Set get all => UnionSet({uses, forwards, metaLoadCss, imports}); + + DependencyReport._( + {required this.uses, required this.forwards, required this.metaLoadCss, required this.imports}); } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index ec4889d00..1f6f55a16 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -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. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 6946b30f2..c39ba869e 100644 --- a/pkg/sass_api/pubspec.yaml +++ b/pkg/sass_api/pubspec.yaml @@ -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 diff --git a/pubspec.yaml b/pubspec.yaml index 530438851..1ff90c37f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 diff --git a/test/cli/shared/watch.dart b/test/cli/shared/watch.dart index 252db8fdb..43432fc05 100644 --- a/test/cli/shared/watch.dart +++ b/test/cli/shared/watch.dart @@ -264,6 +264,100 @@ void sharedTests(Future runSass(Iterable 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();