From 7e371666f4f4554bb4e78de3087fec8177074b29 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 14 Jun 2021 17:41:56 -0700 Subject: [PATCH] Add quietDeps and verbose to the JS API (#1353) To support this, we now run Node-Sass-style relative loads outside of the Node importer. This allows the evaluator to check whether a relative load succeeded and use that to determine whether the stylesheet counts as a dependency. See sass/sass#3065 --- CHANGELOG.md | 10 +- lib/src/async_import_cache.dart | 4 +- lib/src/import_cache.dart | 6 +- lib/src/importer/node/implementation.dart | 60 ++++---- lib/src/importer/node/interface.dart | 4 + lib/src/node.dart | 8 ++ lib/src/node/render_options.dart | 6 +- lib/src/visitor/async_evaluate.dart | 94 +++++++----- lib/src/visitor/evaluate.dart | 95 ++++++++----- pubspec.yaml | 2 +- test/node_api_test.dart | 165 ++++++++++++++++++++++ 11 files changed, 351 insertions(+), 103 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2113d67d2..b7f347b3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -## 1.34.2 +## 1.35.0 * Fix a couple bugs that could prevent some members from being found in certain files that use a mix of imports and the module system. @@ -6,6 +6,14 @@ * Fix incorrect recommendation for migrating division expressions that reference namespaced variables. +### JS API + +* Add a `quietDeps` option which silences compiler warnings from stylesheets + loaded through importers and load paths. + +* Add a `verbose` option which causes the compiler to emit all deprecation + warnings, not just 5 per feature. + ## 1.34.1 * Fix a bug where `--update` would always compile any file that depends on a diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 70944fc8b..9c454b42c 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -102,8 +102,8 @@ class AsyncImportCache { /// canonicalize [url] (resolved relative to [baseUrl] if it's passed). /// /// If any importers understand [url], returns that importer as well as the - /// canonicalized URL and the original URL resolved relative to [baseUrl] if - /// applicable. Otherwise, returns `null`. + /// canonicalized URL and the original URL (resolved relative to [baseUrl] if + /// applicable). Otherwise, returns `null`. Future?> canonicalize(Uri url, {AsyncImporter? baseImporter, Uri? baseUrl, diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 65bd7b72b..c8fa06961 100644 --- a/lib/src/import_cache.dart +++ b/lib/src/import_cache.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_import_cache.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 6821c9a63333c3c99b0c9515aa04e73a14e0f141 +// Checksum: 27d8582c2ab318a52d433390ec256497b6af5dec // // ignore_for_file: unused_import @@ -108,8 +108,8 @@ class ImportCache { /// canonicalize [url] (resolved relative to [baseUrl] if it's passed). /// /// If any importers understand [url], returns that importer as well as the - /// canonicalized URL and the original URL resolved relative to [baseUrl] if - /// applicable. Otherwise, returns `null`. + /// canonicalized URL and the original URL (resolved relative to [baseUrl] if + /// applicable). Otherwise, returns `null`. Tuple3? canonicalize(Uri url, {Importer? baseImporter, Uri? baseUrl, bool forImport = false}) { if (baseImporter != null) { diff --git a/lib/src/importer/node/implementation.dart b/lib/src/importer/node/implementation.dart index 823ec5914..2c350be7f 100644 --- a/lib/src/importer/node/implementation.dart +++ b/lib/src/importer/node/implementation.dart @@ -68,18 +68,33 @@ class NodeImporter { yield* sassPath.split(isWindows ? ';' : ':'); } - /// Loads the stylesheet at [url]. + /// Loads the stylesheet at [url] relative to [previous] if possible. + /// + /// This can also load [url] directly if it's an absolute `file:` URL, even if + /// `previous` isn't defined or isn't a `file:` URL. + /// + /// Returns the stylesheet at that path and the URL used to load it, or `null` + /// if loading failed. + Tuple2? loadRelative( + String url, Uri? previous, bool forImport) { + if (p.url.isAbsolute(url)) { + if (!url.startsWith('/') && !url.startsWith('file:')) return null; + return _tryPath(p.fromUri(url), forImport); + } + + if (previous?.scheme != 'file') return null; + + // 1: Filesystem imports relative to the base file. + return _tryPath( + p.join(p.dirname(p.fromUri(previous)), p.fromUri(url)), forImport); + } + + /// Loads the stylesheet at [url] from an importer or load path. /// /// The [previous] URL is the URL of the stylesheet in which the import /// appeared. Returns the contents of the stylesheet and the URL to use as /// [previous] for imports within the loaded stylesheet. Tuple2? load(String url, Uri? previous, bool forImport) { - var parsed = Uri.parse(url); - if (parsed.scheme == '' || parsed.scheme == 'file') { - var result = _resolveRelativePath(p.fromUri(parsed), previous, forImport); - if (result != null) return result; - } - // The previous URL is always an absolute file path for filesystem imports. var previousString = _previousToString(previous); for (var importer in _importers) { @@ -90,22 +105,17 @@ class NodeImporter { } } - return _resolveLoadPathFromUrl(parsed, forImport); + return _resolveLoadPathFromUrl(Uri.parse(url), forImport); } - /// Asynchronously loads the stylesheet at [url]. + /// Asynchronously loads the stylesheet at [url] from an importer or load + /// path. /// /// The [previous] URL is the URL of the stylesheet in which the import /// appeared. Returns the contents of the stylesheet and the URL to use as /// [previous] for imports within the loaded stylesheet. Future?> loadAsync( String url, Uri? previous, bool forImport) async { - var parsed = Uri.parse(url); - if (parsed.scheme == '' || parsed.scheme == 'file') { - var result = _resolveRelativePath(p.fromUri(parsed), previous, forImport); - if (result != null) return result; - } - // The previous URL is always an absolute file path for filesystem imports. var previousString = _previousToString(previous); for (var importer in _importers) { @@ -116,20 +126,7 @@ class NodeImporter { } } - return _resolveLoadPathFromUrl(parsed, forImport); - } - - /// Tries to load a stylesheet at the given [path] relative to [previous]. - /// - /// Returns the stylesheet at that path and the URL used to load it, or `null` - /// if loading failed. - Tuple2? _resolveRelativePath( - String path, Uri? previous, bool forImport) { - if (p.isAbsolute(path)) return _tryPath(path, forImport); - if (previous?.scheme != 'file') return null; - - // 1: Filesystem imports relative to the base file. - return _tryPath(p.join(p.dirname(p.fromUri(previous)), path), forImport); + return _resolveLoadPathFromUrl(Uri.parse(url), forImport); } /// Converts [previous] to a string to pass to the importer function. @@ -192,8 +189,9 @@ class NodeImporter { } else if (contents != null) { return Tuple2(contents, file); } else { - var resolved = _resolveRelativePath(file, previous, forImport) ?? - _resolveLoadPath(file, forImport); + var resolved = + loadRelative(p.toUri(file).toString(), previous, forImport) ?? + _resolveLoadPath(file, forImport); if (resolved != null) return resolved; throw "Can't find stylesheet to import."; } diff --git a/lib/src/importer/node/interface.dart b/lib/src/importer/node/interface.dart index e280d27bc..5cef8cd30 100644 --- a/lib/src/importer/node/interface.dart +++ b/lib/src/importer/node/interface.dart @@ -8,6 +8,10 @@ class NodeImporter { NodeImporter(Object options, Iterable includePaths, Iterable importers); + Tuple2? loadRelative( + String url, Uri? previous, bool forImport) => + throw ''; + Tuple2? load(String url, Uri? previous, bool forImport) => throw ''; diff --git a/lib/src/node.dart b/lib/src/node.dart index 5a430dac2..f37ff4747 100644 --- a/lib/src/node.dart +++ b/lib/src/node.dart @@ -106,6 +106,8 @@ Future _renderAsync(RenderOptions options) async { indentWidth: _parseIndentWidth(options.indentWidth), lineFeed: _parseLineFeed(options.linefeed), url: file == null ? 'stdin' : p.toUri(file).toString(), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, sourceMap: _enableSourceMaps(options)); } else if (file != null) { result = await compileAsync(file, @@ -116,6 +118,8 @@ Future _renderAsync(RenderOptions options) async { useSpaces: options.indentType != 'tab', indentWidth: _parseIndentWidth(options.indentWidth), lineFeed: _parseLineFeed(options.linefeed), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, sourceMap: _enableSourceMaps(options)); } else { throw ArgumentError("Either options.data or options.file must be set."); @@ -147,6 +151,8 @@ RenderResult _renderSync(RenderOptions options) { indentWidth: _parseIndentWidth(options.indentWidth), lineFeed: _parseLineFeed(options.linefeed), url: file == null ? 'stdin' : p.toUri(file).toString(), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, sourceMap: _enableSourceMaps(options)); } else if (file != null) { result = compile(file, @@ -157,6 +163,8 @@ RenderResult _renderSync(RenderOptions options) { useSpaces: options.indentType != 'tab', indentWidth: _parseIndentWidth(options.indentWidth), lineFeed: _parseLineFeed(options.linefeed), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, sourceMap: _enableSourceMaps(options)); } else { throw ArgumentError("Either options.data or options.file must be set."); diff --git a/lib/src/node/render_options.dart b/lib/src/node/render_options.dart index 9b6dea08d..603a23cd5 100644 --- a/lib/src/node/render_options.dart +++ b/lib/src/node/render_options.dart @@ -26,6 +26,8 @@ class RenderOptions { external bool? get sourceMapContents; external bool? get sourceMapEmbed; external String? get sourceMapRoot; + external bool? get quietDeps; + external bool? get verbose; external factory RenderOptions( {String? file, @@ -44,5 +46,7 @@ class RenderOptions { Object? sourceMap, bool? sourceMapContents, bool? sourceMapEmbed, - String? sourceMapRoot}); + String? sourceMapRoot, + bool? quietDeps, + bool? verbose}); } diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index d3aad9d84..b32546075 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -157,13 +157,7 @@ class _EvaluateVisitor /// consoles with redundant warnings. final _warningsEmitted = >{}; - // The importer from which the entrypoint stylesheet was loaded. - late final AsyncImporter? _originalImporter; - /// Whether to avoid emitting warnings for files loaded from dependencies. - /// - /// A "dependency" in this context is any stylesheet loaded through an - /// importer other than [_originalImporter]. final bool _quietDeps; /// Whether to track source map information. @@ -266,7 +260,7 @@ class _EvaluateVisitor /// /// A dependency is defined as a stylesheet imported by an importer other than /// the original. In Node importers, nothing is considered a dependency. - bool get _inDependency => !_asNodeSass && _importer != _originalImporter; + var _inDependency = false; /// The stylesheet that's currently being evaluated. Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet"); @@ -523,7 +517,6 @@ class _EvaluateVisitor } } - _originalImporter = importer; var module = await _execute(importer, node); return EvaluateResult(_combineCss(module), _includedFiles); @@ -620,8 +613,7 @@ class _EvaluateVisitor await _withStackFrame(stackFrame, nodeWithSpan, () async { var result = await _loadStylesheet(url.toString(), nodeWithSpan.span, baseUrl: baseUrl); - var importer = result.item1; - var stylesheet = result.item2; + var stylesheet = result.stylesheet; var canonicalUrl = stylesheet.span.sourceUrl; if (canonicalUrl != null && _activeModules.containsKey(canonicalUrl)) { @@ -637,14 +629,17 @@ class _EvaluateVisitor } if (canonicalUrl != null) _activeModules[canonicalUrl] = nodeWithSpan; + var oldInDependency = _inDependency; + _inDependency = result.isDependency; Module module; try { - module = await _execute(importer, stylesheet, + module = await _execute(result.importer, stylesheet, configuration: configuration, nodeWithSpan: nodeWithSpan, namesInErrors: namesInErrors); } finally { _activeModules.remove(canonicalUrl); + _inDependency = oldInDependency; } try { @@ -1465,8 +1460,7 @@ class _EvaluateVisitor return _withStackFrame("@import", import, () async { var result = await _loadStylesheet(import.url, import.span, forImport: true); - var importer = result.item1; - var stylesheet = result.item2; + var stylesheet = result.stylesheet; var url = stylesheet.span.sourceUrl; if (url != null) { @@ -1486,7 +1480,7 @@ class _EvaluateVisitor if (stylesheet.uses.isEmpty && stylesheet.forwards.isEmpty) { var oldImporter = _importer; var oldStylesheet = _stylesheet; - _importer = importer; + _importer = result.importer; _stylesheet = stylesheet; await visitStylesheet(stylesheet); _importer = oldImporter; @@ -1505,12 +1499,14 @@ class _EvaluateVisitor var oldEndOfImports = _endOfImports; var oldOutOfOrderImports = _outOfOrderImports; var oldConfiguration = _configuration; - _importer = importer; + var oldInDependency = _inDependency; + _importer = result.importer; _stylesheet = stylesheet; _root = ModifiableCssStylesheet(stylesheet.span); _parent = _root; _endOfImports = 0; _outOfOrderImports = null; + _inDependency = result.isDependency; // This configuration is only used if it passes through a `@forward` // rule, so we avoid creating unnecessary ones for performance reasons. @@ -1528,6 +1524,7 @@ class _EvaluateVisitor _endOfImports = oldEndOfImports; _outOfOrderImports = oldOutOfOrderImports; _configuration = oldConfiguration; + _inDependency = oldInDependency; }); // Create a dummy module with empty CSS and no extensions to make forwarded @@ -1559,8 +1556,7 @@ class _EvaluateVisitor /// /// This first tries loading [url] relative to [baseUrl], which defaults to /// `_stylesheet.span.sourceUrl`. - Future> _loadStylesheet( - String url, FileSpan span, + Future<_LoadedStylesheet> _loadStylesheet(String url, FileSpan span, {Uri? baseUrl, bool forImport = false}) async { try { assert(_importSpan == null); @@ -1568,21 +1564,23 @@ class _EvaluateVisitor var importCache = _importCache; if (importCache != null) { + baseUrl ??= _stylesheet.span.sourceUrl; var tuple = await importCache.canonicalize(Uri.parse(url), - baseImporter: _importer, - baseUrl: baseUrl ?? _stylesheet.span.sourceUrl, - forImport: forImport); + baseImporter: _importer, baseUrl: baseUrl, forImport: forImport); if (tuple != null) { + var isDependency = _inDependency || tuple.item1 != _importer; var stylesheet = await importCache.importCanonical( tuple.item1, tuple.item2, - originalUrl: tuple.item3, - quiet: _quietDeps && tuple.item1 != _originalImporter); - if (stylesheet != null) return Tuple2(tuple.item1, stylesheet); + originalUrl: tuple.item3, quiet: _quietDeps && isDependency); + if (stylesheet != null) { + return _LoadedStylesheet(stylesheet, + importer: tuple.item1, isDependency: isDependency); + } } } else { - var stylesheet = await _importLikeNode(url, forImport); - if (stylesheet != null) return Tuple2(null, stylesheet); + var result = await _importLikeNode(url, forImport); + if (result != null) return result; } if (url.startsWith('package:') && isNode) { @@ -1610,20 +1608,32 @@ class _EvaluateVisitor /// Imports a stylesheet using [_nodeImporter]. /// /// Returns the [Stylesheet], or `null` if the import failed. - Future _importLikeNode( + Future<_LoadedStylesheet?> _importLikeNode( String originalUrl, bool forImport) async { - var result = await _nodeImporter! - .loadAsync(originalUrl, _stylesheet.span.sourceUrl, forImport); - if (result == null) return null; + var result = _nodeImporter! + .loadRelative(originalUrl, _stylesheet.span.sourceUrl, forImport); + + bool isDependency; + if (result != null) { + isDependency = _inDependency; + } else { + result = await _nodeImporter! + .loadAsync(originalUrl, _stylesheet.span.sourceUrl, forImport); + if (result == null) return null; + isDependency = true; + } var contents = result.item1; var url = result.item2; _includedFiles.add(url.startsWith('file:') ? p.fromUri(url) : url); - return Stylesheet.parse( - contents, url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, - url: url, logger: _logger); + return _LoadedStylesheet( + Stylesheet.parse(contents, + url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, + url: url, + logger: _quietDeps && isDependency ? Logger.quiet : _logger), + isDependency: isDependency); } /// Adds a CSS import for [import]. @@ -3341,3 +3351,23 @@ class _ArgumentResults { _ArgumentResults(this.positional, this.positionalNodes, this.named, this.namedNodes, this.separator); } + +/// The result of loading a stylesheet via [AsyncEvaluator._loadStylesheet]. +class _LoadedStylesheet { + /// The stylesheet itself. + final Stylesheet stylesheet; + + /// The importer that was used to load the stylesheet. + /// + /// This is `null` when running in Node Sass compatibility mode. + final AsyncImporter? importer; + + /// Whether this load counts as a dependency. + /// + /// That is, whether this was (transitively) loaded through a load path or + /// importer rather than relative to the entrypoint. + final bool isDependency; + + _LoadedStylesheet(this.stylesheet, + {this.importer, required this.isDependency}); +} diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index f61b28444..fa49cd546 100644 --- a/lib/src/visitor/evaluate.dart +++ b/lib/src/visitor/evaluate.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_evaluate.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 24b9012f1cf8908b2cbde11cd10974113d4c8163 +// Checksum: 203ba98fd9ca92ccc0b6465fd0d617e88c8dd37e // // ignore_for_file: unused_import @@ -165,13 +165,7 @@ class _EvaluateVisitor /// consoles with redundant warnings. final _warningsEmitted = >{}; - // The importer from which the entrypoint stylesheet was loaded. - late final Importer? _originalImporter; - /// Whether to avoid emitting warnings for files loaded from dependencies. - /// - /// A "dependency" in this context is any stylesheet loaded through an - /// importer other than [_originalImporter]. final bool _quietDeps; /// Whether to track source map information. @@ -274,7 +268,7 @@ class _EvaluateVisitor /// /// A dependency is defined as a stylesheet imported by an importer other than /// the original. In Node importers, nothing is considered a dependency. - bool get _inDependency => !_asNodeSass && _importer != _originalImporter; + var _inDependency = false; /// The stylesheet that's currently being evaluated. Stylesheet get _stylesheet => _assertInModule(__stylesheet, "_stylesheet"); @@ -528,7 +522,6 @@ class _EvaluateVisitor } } - _originalImporter = importer; var module = _execute(importer, node); return EvaluateResult(_combineCss(module), _includedFiles); @@ -625,8 +618,7 @@ class _EvaluateVisitor _withStackFrame(stackFrame, nodeWithSpan, () { var result = _loadStylesheet(url.toString(), nodeWithSpan.span, baseUrl: baseUrl); - var importer = result.item1; - var stylesheet = result.item2; + var stylesheet = result.stylesheet; var canonicalUrl = stylesheet.span.sourceUrl; if (canonicalUrl != null && _activeModules.containsKey(canonicalUrl)) { @@ -642,14 +634,17 @@ class _EvaluateVisitor } if (canonicalUrl != null) _activeModules[canonicalUrl] = nodeWithSpan; + var oldInDependency = _inDependency; + _inDependency = result.isDependency; Module module; try { - module = _execute(importer, stylesheet, + module = _execute(result.importer, stylesheet, configuration: configuration, nodeWithSpan: nodeWithSpan, namesInErrors: namesInErrors); } finally { _activeModules.remove(canonicalUrl); + _inDependency = oldInDependency; } try { @@ -1464,8 +1459,7 @@ class _EvaluateVisitor void _visitDynamicImport(DynamicImport import) { return _withStackFrame("@import", import, () { var result = _loadStylesheet(import.url, import.span, forImport: true); - var importer = result.item1; - var stylesheet = result.item2; + var stylesheet = result.stylesheet; var url = stylesheet.span.sourceUrl; if (url != null) { @@ -1485,7 +1479,7 @@ class _EvaluateVisitor if (stylesheet.uses.isEmpty && stylesheet.forwards.isEmpty) { var oldImporter = _importer; var oldStylesheet = _stylesheet; - _importer = importer; + _importer = result.importer; _stylesheet = stylesheet; visitStylesheet(stylesheet); _importer = oldImporter; @@ -1504,12 +1498,14 @@ class _EvaluateVisitor var oldEndOfImports = _endOfImports; var oldOutOfOrderImports = _outOfOrderImports; var oldConfiguration = _configuration; - _importer = importer; + var oldInDependency = _inDependency; + _importer = result.importer; _stylesheet = stylesheet; _root = ModifiableCssStylesheet(stylesheet.span); _parent = _root; _endOfImports = 0; _outOfOrderImports = null; + _inDependency = result.isDependency; // This configuration is only used if it passes through a `@forward` // rule, so we avoid creating unnecessary ones for performance reasons. @@ -1527,6 +1523,7 @@ class _EvaluateVisitor _endOfImports = oldEndOfImports; _outOfOrderImports = oldOutOfOrderImports; _configuration = oldConfiguration; + _inDependency = oldInDependency; }); // Create a dummy module with empty CSS and no extensions to make forwarded @@ -1558,7 +1555,7 @@ class _EvaluateVisitor /// /// This first tries loading [url] relative to [baseUrl], which defaults to /// `_stylesheet.span.sourceUrl`. - Tuple2 _loadStylesheet(String url, FileSpan span, + _LoadedStylesheet _loadStylesheet(String url, FileSpan span, {Uri? baseUrl, bool forImport = false}) { try { assert(_importSpan == null); @@ -1566,20 +1563,22 @@ class _EvaluateVisitor var importCache = _importCache; if (importCache != null) { + baseUrl ??= _stylesheet.span.sourceUrl; var tuple = importCache.canonicalize(Uri.parse(url), - baseImporter: _importer, - baseUrl: baseUrl ?? _stylesheet.span.sourceUrl, - forImport: forImport); + baseImporter: _importer, baseUrl: baseUrl, forImport: forImport); if (tuple != null) { + var isDependency = _inDependency || tuple.item1 != _importer; var stylesheet = importCache.importCanonical(tuple.item1, tuple.item2, - originalUrl: tuple.item3, - quiet: _quietDeps && tuple.item1 != _originalImporter); - if (stylesheet != null) return Tuple2(tuple.item1, stylesheet); + originalUrl: tuple.item3, quiet: _quietDeps && isDependency); + if (stylesheet != null) { + return _LoadedStylesheet(stylesheet, + importer: tuple.item1, isDependency: isDependency); + } } } else { - var stylesheet = _importLikeNode(url, forImport); - if (stylesheet != null) return Tuple2(null, stylesheet); + var result = _importLikeNode(url, forImport); + if (result != null) return result; } if (url.startsWith('package:') && isNode) { @@ -1607,19 +1606,31 @@ class _EvaluateVisitor /// Imports a stylesheet using [_nodeImporter]. /// /// Returns the [Stylesheet], or `null` if the import failed. - Stylesheet? _importLikeNode(String originalUrl, bool forImport) { - var result = - _nodeImporter!.load(originalUrl, _stylesheet.span.sourceUrl, forImport); - if (result == null) return null; + _LoadedStylesheet? _importLikeNode(String originalUrl, bool forImport) { + var result = _nodeImporter! + .loadRelative(originalUrl, _stylesheet.span.sourceUrl, forImport); + + bool isDependency; + if (result != null) { + isDependency = _inDependency; + } else { + result = _nodeImporter! + .load(originalUrl, _stylesheet.span.sourceUrl, forImport); + if (result == null) return null; + isDependency = true; + } var contents = result.item1; var url = result.item2; _includedFiles.add(url.startsWith('file:') ? p.fromUri(url) : url); - return Stylesheet.parse( - contents, url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, - url: url, logger: _logger); + return _LoadedStylesheet( + Stylesheet.parse(contents, + url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, + url: url, + logger: _quietDeps && isDependency ? Logger.quiet : _logger), + isDependency: isDependency); } /// Adds a CSS import for [import]. @@ -3278,3 +3289,23 @@ class _ArgumentResults { _ArgumentResults(this.positional, this.positionalNodes, this.named, this.namedNodes, this.separator); } + +/// The result of loading a stylesheet via [Evaluator._loadStylesheet]. +class _LoadedStylesheet { + /// The stylesheet itself. + final Stylesheet stylesheet; + + /// The importer that was used to load the stylesheet. + /// + /// This is `null` when running in Node Sass compatibility mode. + final Importer? importer; + + /// Whether this load counts as a dependency. + /// + /// That is, whether this was (transitively) loaded through a load path or + /// importer rather than relative to the entrypoint. + final bool isDependency; + + _LoadedStylesheet(this.stylesheet, + {this.importer, required this.isDependency}); +} diff --git a/pubspec.yaml b/pubspec.yaml index 327c2c250..304603698 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.34.2-dev +version: 1.35.0 description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/node_api_test.dart b/test/node_api_test.dart index 3146dbbd4..65d28a2f7 100644 --- a/test/node_api_test.dart +++ b/test/node_api_test.dart @@ -264,6 +264,171 @@ a { renderSync(RenderOptions(data: "@debug 'what the heck'")), isEmpty); }); + group("with quietDeps", () { + group("in a relative load from the entrypoint", () { + test("emits @warn", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await writeTextFile(p.join(sandbox, "_other.scss"), "@warn heck"); + + expect(const LineSplitter().bind(interceptStderr()), + emitsThrough(contains("heck"))); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), quietDeps: true)); + }); + + test("emits @debug", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await writeTextFile(p.join(sandbox, "_other.scss"), "@debug heck"); + + expect(const LineSplitter().bind(interceptStderr()), + emitsThrough(contains("heck"))); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), quietDeps: true)); + }); + + test("emits parser warnings", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await writeTextFile(p.join(sandbox, "_other.scss"), "a {b: c && d}"); + + expect(const LineSplitter().bind(interceptStderr()), + emitsThrough(contains("&&"))); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), quietDeps: true)); + }); + + test("emits runner warnings", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await writeTextFile(p.join(sandbox, "_other.scss"), "#{blue} {x: y}"); + + expect(const LineSplitter().bind(interceptStderr()), + emitsThrough(contains("blue"))); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), quietDeps: true)); + }); + }); + + group("in a load path load", () { + test("emits @warn", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await createDirectory(p.join(sandbox, "dir")); + await writeTextFile( + p.join(sandbox, "dir", "_other.scss"), "@warn heck"); + + expect(const LineSplitter().bind(interceptStderr()), + emitsThrough(contains("heck"))); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), + includePaths: [p.join(sandbox, "dir")], + quietDeps: true)); + }); + + test("emits @debug", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await createDirectory(p.join(sandbox, "dir")); + await writeTextFile( + p.join(sandbox, "dir", "_other.scss"), "@debug heck"); + + expect(const LineSplitter().bind(interceptStderr()), + emitsThrough(contains("heck"))); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), + includePaths: [p.join(sandbox, "dir")], + quietDeps: true)); + }); + + test("doesn't emit parser warnings", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await createDirectory(p.join(sandbox, "dir")); + await writeTextFile( + p.join(sandbox, "dir", "_other.scss"), "a {b: c && d}"); + + // No stderr should be printed at all. + const LineSplitter() + .bind(interceptStderr()) + .listen(expectAsync1((_) {}, count: 0)); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), + includePaths: [p.join(sandbox, "dir")], + quietDeps: true)); + + // Give stderr a chance to be piped through if it's going to be. + await pumpEventQueue(); + }); + + test("doesn't emit runner warnings", () async { + await writeTextFile(p.join(sandbox, "test.scss"), "@use 'other'"); + await createDirectory(p.join(sandbox, "dir")); + await writeTextFile( + p.join(sandbox, "dir", "_other.scss"), "#{blue} {x: y}"); + + // No stderr should be printed at all. + const LineSplitter() + .bind(interceptStderr()) + .listen(expectAsync1((_) {}, count: 0)); + + renderSync(RenderOptions( + file: p.join(sandbox, "test.scss"), + includePaths: [p.join(sandbox, "dir")], + quietDeps: true)); + + // Give stderr a chance to be piped through if it's going to be. + await pumpEventQueue(); + }); + }); + }); + + group("with a bunch of deprecation warnings", () { + setUp(() async { + await writeTextFile(p.join(sandbox, "test.scss"), r""" + $_: call("inspect", null); + $_: call("rgb", 0, 0, 0); + $_: call("nth", null, 1); + $_: call("join", null, null); + $_: call("if", true, 1, 2); + $_: call("hsl", 0, 100%, 100%); + + $_: 1/2; + $_: 1/3; + $_: 1/4; + $_: 1/5; + $_: 1/6; + $_: 1/7; + """); + }); + + test("without --verbose, only prints five", () async { + expect( + const LineSplitter().bind(interceptStderr()), + emitsInOrder([ + ...List.filled(5, emitsThrough(contains("call()"))), + ...List.filled(5, emitsThrough(contains("math.div"))), + emitsThrough( + contains("2 repetitive deprecation warnings omitted.")) + ])); + + renderSync(RenderOptions(file: p.join(sandbox, "test.scss"))); + }); + + test("with --verbose, prints all", () async { + expect( + const LineSplitter().bind(interceptStderr()), + emitsInOrder([ + ...List.filled(6, emitsThrough(contains("call()"))), + ...List.filled(6, emitsThrough(contains("math.div"))) + ])); + + renderSync( + RenderOptions(file: p.join(sandbox, "test.scss"), verbose: true)); + }); + }); + group("with both data and file", () { test("uses the data parameter as the source", () { expect(renderSync(RenderOptions(data: "x {y: z}", file: sassPath)),