diff --git a/CHANGELOG.md b/CHANGELOG.md index 3096f4dc..d6dfbdf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,22 @@ +## 1.36.0 + +### Dart API + +* Added `compileToResult()`, `compileStringToResult()`, + `compileToResultAsync()`, and `compileStringToResultAsync()` methods. These + are intended to replace the existing `compile*()` methods, which are now + deprecated. Rather than returning a simple string, these return a + `CompileResult` object, which will allow us to add additional information + about the compilation without having to introduce further deprecations. + + * Instead of passing a `sourceMaps` callback to `compile*()`, pass + `sourceMaps: true` to `compile*ToResult()` and access + `CompileResult.sourceMap`. + + * The `CompileResult` object exposes a `includedUrls` object which lists the + canonical URLs accessed during a compilation. This information was + previously unavailable except through the JS API. + ## 1.35.2 * **Potentially breaking bug fix**: Properly throw an error for Unicode ranges diff --git a/lib/sass.dart b/lib/sass.dart index 10619293..8f73c120 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -11,6 +11,7 @@ import 'package:source_maps/source_maps.dart'; import 'src/async_import_cache.dart'; import 'src/callable.dart'; import 'src/compile.dart' as c; +import 'src/compile_result.dart'; import 'src/exception.dart'; import 'src/import_cache.dart'; import 'src/importer.dart'; @@ -20,6 +21,7 @@ import 'src/util/nullable.dart'; import 'src/visitor/serialize.dart'; export 'src/callable.dart' show Callable, AsyncCallable; +export 'src/compile_result.dart'; export 'src/exception.dart' show SassException; export 'src/importer.dart'; export 'src/logger.dart'; @@ -28,7 +30,9 @@ export 'src/value.dart'; export 'src/visitor/serialize.dart' show OutputStyle; export 'src/warn.dart' show warn; -/// Loads the Sass file at [path], compiles it to CSS, and returns the result. +/// Loads the Sass file at [path], compiles it to CSS, and returns a +/// [CompileResult] containing the CSS and additional metadata about the +/// compilation. /// /// If [color] is `true`, this will use terminal colors in warnings. It's /// ignored if [logger] is passed. @@ -67,12 +71,13 @@ export 'src/warn.dart' show warn; /// times, further warnings for that feature are silenced. If [verbose] is true, /// all deprecation warnings are printed instead. /// -/// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which -/// sections of the source file(s) correspond to which in the resulting CSS. -/// It's called immediately before this method returns, and only if compilation -/// succeeds. Note that [SingleMapping.targetUrl] will always be `null`. Users -/// using the [SourceMap] API should be sure to add the [`source_maps`][] -/// package to their pubspec. +/// If [sourceMap] is `true`, [CompileResult.sourceMap] will be set to a +/// [SingleMapping] that indicates which sections of the source file(s) +/// correspond to which in the resulting CSS. [SingleMapping.targetUrl] will be +/// `null`. It's up to the caller to save this mapping to disk and add a source +/// map comment to [CompileResult.css] pointing to it. Users using the +/// [SourceMap] API should be sure to add the [`source_maps`][] package to their +/// pubspec. /// /// [`source_maps`]: https://pub.dartlang.org/packages/source_maps /// @@ -83,46 +88,35 @@ export 'src/warn.dart' show warn; /// /// [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 /// -/// This parameter is meant to be used as an out parameter, so that users who -/// want access to the source map can get it. For example: -/// -/// ```dart -/// SingleMapping sourceMap; -/// var css = compile(sassPath, sourceMap: (map) => sourceMap = map); -/// ``` -/// /// Throws a [SassException] if conversion fails. -String compile(String path, - {bool color = false, - Logger? logger, - Iterable? importers, - Iterable? loadPaths, - PackageConfig? packageConfig, - Iterable? functions, - OutputStyle? style, - bool quietDeps = false, - bool verbose = false, - void sourceMap(SingleMapping map)?, - bool charset = true}) { - logger ??= Logger.stderr(color: color); - var result = c.compile(path, - logger: logger, - importCache: ImportCache( - importers: importers, - logger: logger, - loadPaths: loadPaths, - packageConfig: packageConfig), - functions: functions, - style: style, - quietDeps: quietDeps, - verbose: verbose, - sourceMap: sourceMap != null, - charset: charset); - result.sourceMap.andThen(sourceMap); - return result.css; -} +CompileResult compileToResult(String path, + {bool color = false, + Logger? logger, + Iterable? importers, + Iterable? loadPaths, + PackageConfig? packageConfig, + Iterable? functions, + OutputStyle? style, + bool quietDeps = false, + bool verbose = false, + bool sourceMap = false, + bool charset = true}) => + c.compile(path, + logger: logger, + importCache: ImportCache( + importers: importers, + logger: logger ?? Logger.stderr(color: color), + loadPaths: loadPaths, + packageConfig: packageConfig), + functions: functions, + style: style, + quietDeps: quietDeps, + verbose: verbose, + sourceMap: sourceMap, + charset: charset); -/// Compiles [source] to CSS and returns the result. +/// Compiles [source] to CSS and returns a [CompileResult] containing the CSS +/// and additional metadata about the compilation.. /// /// This parses the stylesheet as [syntax], which defaults to [Syntax.scss]. /// @@ -167,12 +161,13 @@ String compile(String path, /// times, further warnings for that feature are silenced. If [verbose] is true, /// all deprecation warnings are printed instead. /// -/// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which -/// sections of the source file(s) correspond to which in the resulting CSS. -/// It's called immediately before this method returns, and only if compilation -/// succeeds. Note that [SingleMapping.targetUrl] will always be `null`. Users -/// using the [SourceMap] API should be sure to add the [`source_maps`][] -/// package to their pubspec. +/// If [sourceMap] is `true`, [CompileResult.sourceMap] will be set to a +/// [SingleMapping] that indicates which sections of the source file(s) +/// correspond to which in the resulting CSS. [SingleMapping.targetUrl] will be +/// `null`. It's up to the caller to save this mapping to disk and add a source +/// map comment to [CompileResult.css] pointing to it. Users using the +/// [SourceMap] API should be sure to add the [`source_maps`][] package to their +/// pubspec. /// /// [`source_maps`]: https://pub.dartlang.org/packages/source_maps /// @@ -183,6 +178,117 @@ String compile(String path, /// /// [byte-order mark]: https://en.wikipedia.org/wiki/Byte_order_mark#UTF-8 /// +/// Throws a [SassException] if conversion fails. +CompileResult compileStringToResult(String source, + {Syntax? syntax, + bool color = false, + Logger? logger, + Iterable? importers, + PackageConfig? packageConfig, + Iterable? loadPaths, + Iterable? functions, + OutputStyle? style, + Importer? importer, + Object? url, + bool quietDeps = false, + bool verbose = false, + bool sourceMap = false, + bool charset = true}) => + c.compileString(source, + syntax: syntax, + logger: logger, + importCache: ImportCache( + importers: importers, + logger: logger ?? Logger.stderr(color: color), + packageConfig: packageConfig, + loadPaths: loadPaths), + functions: functions, + style: style, + importer: importer, + url: url, + quietDeps: quietDeps, + verbose: verbose, + sourceMap: sourceMap, + charset: charset); + +/// Like [compileToResult], except it runs asynchronously. +/// +/// Running asynchronously allows this to take [AsyncImporter]s rather than +/// synchronous [Importer]s. However, running asynchronously is also somewhat +/// slower, so [compileToResult] should be preferred if possible. +Future compileToResultAsync(String path, + {bool color = false, + Logger? logger, + Iterable? importers, + PackageConfig? packageConfig, + Iterable? loadPaths, + Iterable? functions, + OutputStyle? style, + bool quietDeps = false, + bool verbose = false, + bool sourceMap = false}) => + c.compileAsync(path, + logger: logger, + importCache: AsyncImportCache( + importers: importers, + logger: logger ?? Logger.stderr(color: color), + loadPaths: loadPaths, + packageConfig: packageConfig), + functions: functions, + style: style, + quietDeps: quietDeps, + verbose: verbose, + sourceMap: sourceMap); + +/// Like [compileStringToResult], except it runs asynchronously. +/// +/// Running asynchronously allows this to take [AsyncImporter]s rather than +/// synchronous [Importer]s. However, running asynchronously is also somewhat +/// slower, so [compileStringToResult] should be preferred if possible. +Future compileStringToResultAsync(String source, + {Syntax? syntax, + bool color = false, + Logger? logger, + Iterable? importers, + PackageConfig? packageConfig, + Iterable? loadPaths, + Iterable? functions, + OutputStyle? style, + AsyncImporter? importer, + Object? url, + bool quietDeps = false, + bool verbose = false, + bool sourceMap = false, + bool charset = true}) => + c.compileStringAsync(source, + syntax: syntax, + logger: logger, + importCache: AsyncImportCache( + importers: importers, + logger: logger ?? Logger.stderr(color: color), + packageConfig: packageConfig, + loadPaths: loadPaths), + functions: functions, + style: style, + importer: importer, + url: url, + quietDeps: quietDeps, + verbose: verbose, + sourceMap: sourceMap, + charset: charset); + +/// Like [compileToResult], but returns [CompileResult.css] rather than +/// returning [CompileResult] directly. +/// +/// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which +/// sections of the source file(s) correspond to which in the resulting CSS. +/// It's called immediately before this method returns, and only if compilation +/// succeeds. Note that [SingleMapping.targetUrl] will always be `null`. Users +/// using the [SourceMap] API should be sure to add the [`source_maps`][] +/// package to their pubspec. +/// +/// [`source_maps`]: https://pub.dartlang.org/packages/source_maps +/// /// This parameter is meant to be used as an out parameter, so that users who /// want access to the source map can get it. For example: /// @@ -190,9 +296,58 @@ String compile(String path, /// SingleMapping sourceMap; /// var css = compile(sassPath, sourceMap: (map) => sourceMap = map); /// ``` +@Deprecated("Use compileToResult() instead.") +String compile( + String path, + {bool color = false, + Logger? logger, + Iterable? importers, + Iterable? loadPaths, + PackageConfig? packageConfig, + Iterable? functions, + OutputStyle? style, + bool quietDeps = false, + bool verbose = false, + @Deprecated("Use CompileResult.sourceMap from compileToResult() instead.") + void sourceMap(SingleMapping map)?, + bool charset = true}) { + var result = compileToResult(path, + logger: logger, + importers: importers, + loadPaths: loadPaths, + packageConfig: packageConfig, + functions: functions, + style: style, + quietDeps: quietDeps, + verbose: verbose, + sourceMap: sourceMap != null, + charset: charset); + result.sourceMap.andThen(sourceMap); + return result.css; +} + +/// Like [compileStringToResult], but returns [CompileResult.css] rather than +/// returning [CompileResult] directly. /// -/// Throws a [SassException] if conversion fails. -String compileString(String source, +/// If [sourceMap] is passed, it's passed a [SingleMapping] that indicates which +/// sections of the source file(s) correspond to which in the resulting CSS. +/// It's called immediately before this method returns, and only if compilation +/// succeeds. Note that [SingleMapping.targetUrl] will always be `null`. Users +/// using the [SourceMap] API should be sure to add the [`source_maps`][] +/// package to their pubspec. +/// +/// [`source_maps`]: https://pub.dartlang.org/packages/source_maps +/// +/// This parameter is meant to be used as an out parameter, so that users who +/// want access to the source map can get it. For example: +/// +/// ```dart +/// SingleMapping sourceMap; +/// var css = compileString(sass, sourceMap: (map) => sourceMap = map); +/// ``` +@Deprecated("Use compileStringToResult() instead.") +String compileString( + String source, {Syntax? syntax, bool color = false, Logger? logger, @@ -205,18 +360,17 @@ String compileString(String source, Object? url, bool quietDeps = false, bool verbose = false, - void sourceMap(SingleMapping map)?, + @Deprecated("Use CompileResult.sourceMap from compileStringToResult() instead.") + void sourceMap(SingleMapping map)?, bool charset = true, - @Deprecated("Use syntax instead.") bool indented = false}) { - logger ??= Logger.stderr(color: color); - var result = c.compileString(source, + @Deprecated("Use syntax instead.") + bool indented = false}) { + var result = compileStringToResult(source, syntax: syntax ?? (indented ? Syntax.sass : Syntax.scss), logger: logger, - importCache: ImportCache( - importers: importers, - logger: logger, - packageConfig: packageConfig, - loadPaths: loadPaths), + importers: importers, + packageConfig: packageConfig, + loadPaths: loadPaths, functions: functions, style: style, importer: importer, @@ -234,7 +388,9 @@ String compileString(String source, /// Running asynchronously allows this to take [AsyncImporter]s rather than /// synchronous [Importer]s. However, running asynchronously is also somewhat /// slower, so [compile] should be preferred if possible. -Future compileAsync(String path, +@Deprecated("Use compileToResultAsync() instead.") +Future compileAsync( + String path, {bool color = false, Logger? logger, Iterable? importers, @@ -244,15 +400,13 @@ Future compileAsync(String path, OutputStyle? style, bool quietDeps = false, bool verbose = false, - void sourceMap(SingleMapping map)?}) async { - logger ??= Logger.stderr(color: color); - var result = await c.compileAsync(path, + @Deprecated("Use CompileResult.sourceMap from compileToResultAsync() instead.") + void sourceMap(SingleMapping map)?}) async { + var result = await compileToResultAsync(path, logger: logger, - importCache: AsyncImportCache( - importers: importers, - logger: logger, - loadPaths: loadPaths, - packageConfig: packageConfig), + importers: importers, + loadPaths: loadPaths, + packageConfig: packageConfig, functions: functions, style: style, quietDeps: quietDeps, @@ -267,7 +421,9 @@ Future compileAsync(String path, /// Running asynchronously allows this to take [AsyncImporter]s rather than /// synchronous [Importer]s. However, running asynchronously is also somewhat /// slower, so [compileString] should be preferred if possible. -Future compileStringAsync(String source, +@Deprecated("Use compileStringToResultAsync() instead.") +Future compileStringAsync( + String source, {Syntax? syntax, bool color = false, Logger? logger, @@ -280,18 +436,17 @@ Future compileStringAsync(String source, Object? url, bool quietDeps = false, bool verbose = false, - void sourceMap(SingleMapping map)?, + @Deprecated("Use CompileResult.sourceMap from compileStringToResultAsync() instead.") + void sourceMap(SingleMapping map)?, bool charset = true, - @Deprecated("Use syntax instead.") bool indented = false}) async { - logger ??= Logger.stderr(color: color); - var result = await c.compileStringAsync(source, + @Deprecated("Use syntax instead.") + bool indented = false}) async { + var result = await compileStringToResultAsync(source, syntax: syntax ?? (indented ? Syntax.sass : Syntax.scss), logger: logger, - importCache: AsyncImportCache( - importers: importers, - logger: logger, - packageConfig: packageConfig, - loadPaths: loadPaths), + importers: importers, + packageConfig: packageConfig, + loadPaths: loadPaths, functions: functions, style: style, importer: importer, diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index 4a7cc6d3..5b7541c5 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -5,11 +5,11 @@ import 'dart:convert'; import 'package:path/path.dart' as p; -import 'package:source_maps/source_maps.dart'; import 'ast/sass.dart'; import 'async_import_cache.dart'; import 'callable.dart'; +import 'compile_result.dart'; import 'importer.dart'; import 'importer/node.dart'; import 'io.dart'; @@ -171,30 +171,3 @@ Future _compileStylesheet( return CompileResult(evaluateResult, serializeResult); } - -/// The result of compiling a Sass document to CSS, along with metadata about -/// the compilation process. -class CompileResult { - /// The result of evaluating the source file. - final EvaluateResult _evaluate; - - /// The result of serializing the CSS AST to CSS text. - final SerializeResult _serialize; - - /// The compiled CSS. - String get css => _serialize.css; - - /// The source map indicating how the source files map to [css]. - /// - /// This is `null` if source mapping was disabled for this compilation. - SingleMapping? get sourceMap => _serialize.sourceMap; - - /// The set that will eventually populate the JS API's - /// `result.stats.includedFiles` field. - /// - /// For filesystem imports, this contains the import path. For all other - /// imports, it contains the URL passed to the `@import`. - Set get includedFiles => _evaluate.includedFiles; - - CompileResult(this._evaluate, this._serialize); -} diff --git a/lib/src/compile.dart b/lib/src/compile.dart index adc07280..d7a43fe3 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,22 +5,20 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: 8e813f2ead6e78899ce820e279983278809a7ea5 +// Checksum: 1a1251aa9f7312612a64760f59803568bd09a07c // // ignore_for_file: unused_import -import 'async_compile.dart'; export 'async_compile.dart'; import 'dart:convert'; import 'package:path/path.dart' as p; -import 'package:source_maps/source_maps.dart'; -import 'package:source_span/source_span.dart'; import 'ast/sass.dart'; import 'import_cache.dart'; import 'callable.dart'; +import 'compile_result.dart'; import 'importer.dart'; import 'importer/node.dart'; import 'io.dart'; diff --git a/lib/src/compile_result.dart b/lib/src/compile_result.dart new file mode 100644 index 00000000..c85d510c --- /dev/null +++ b/lib/src/compile_result.dart @@ -0,0 +1,35 @@ +// Copyright 2021 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 'package:meta/meta.dart'; +import 'package:source_maps/source_maps.dart'; + +import 'visitor/async_evaluate.dart'; +import 'visitor/serialize.dart'; + +/// The result of compiling a Sass document to CSS, along with metadata about +/// the compilation process. +@sealed +class CompileResult { + /// The result of evaluating the source file. + final EvaluateResult _evaluate; + + /// The result of serializing the CSS AST to CSS text. + final SerializeResult _serialize; + + /// The compiled CSS. + String get css => _serialize.css; + + /// The source map indicating how the source files map to [css]. + /// + /// This is `null` if source mapping was disabled for this compilation. + SingleMapping? get sourceMap => _serialize.sourceMap; + + /// The canonical URLs of all stylesheets loaded during compilation. + Set get includedUrls => _evaluate.includedUrls; + + /// @nodoc + @internal + CompileResult(this._evaluate, this._serialize); +} diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 23a90121..f9e61cb0 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -9,6 +9,7 @@ import 'package:source_maps/source_maps.dart'; import '../async_import_cache.dart'; import '../compile.dart'; +import '../compile_result.dart'; import '../exception.dart'; import '../importer/filesystem.dart'; import '../io.dart'; diff --git a/lib/src/importer/node/implementation.dart b/lib/src/importer/node/implementation.dart index 2c350be7..e6446620 100644 --- a/lib/src/importer/node/implementation.dart +++ b/lib/src/importer/node/implementation.dart @@ -27,9 +27,8 @@ import '../utils.dart'; /// imported by a different importer. /// /// * Importers can return file paths rather than the contents of the imported -/// file. These paths are made absolute before they're included in -/// [EvaluateResult.includedFiles] or passed as the previous "URL" to other -/// importers. +/// file. These paths are made absolute before they'repassed as the previous +/// "URL" to other importers. /// /// * The working directory is always implicitly an include path. /// diff --git a/lib/src/node.dart b/lib/src/node.dart index f37ff474..7f7d40d2 100644 --- a/lib/src/node.dart +++ b/lib/src/node.dart @@ -15,6 +15,7 @@ import 'package:tuple/tuple.dart'; import 'ast/sass.dart'; import 'callable.dart'; import 'compile.dart'; +import 'compile_result.dart'; import 'exception.dart'; import 'io.dart'; import 'importer/node.dart'; @@ -408,7 +409,10 @@ RenderResult _newRenderResult( start: start.millisecondsSinceEpoch, end: end.millisecondsSinceEpoch, duration: end.difference(start).inMilliseconds, - includedFiles: result.includedFiles.toList())); + includedFiles: [ + for (var url in result.includedUrls) + if (url.scheme == 'file') p.fromUri(url) else url.toString() + ])); } /// Returns whether source maps are enabled by [options]. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 4b3d7340..5d4e8f01 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -216,12 +216,8 @@ class _EvaluateVisitor /// Whether we're currently building the output of a `@keyframes` rule. var _inKeyframes = false; - /// The set that will eventually populate the JS API's - /// `result.stats.includedFiles` field. - /// - /// For filesystem imports, this contains the import path. For all other - /// imports, it contains the URL passed to the `@import`. - final _includedFiles = {}; + /// The canonical URLs of all stylesheets loaded during compilation. + final _includedUrls = {}; /// A map from canonical URLs for modules (or imported files) that are /// currently being evaluated to AST nodes whose spans indicate the original @@ -508,18 +504,12 @@ class _EvaluateVisitor var url = node.span.sourceUrl; if (url != null) { _activeModules[url] = null; - if (_asNodeSass) { - if (url.scheme == 'file') { - _includedFiles.add(p.fromUri(url)); - } else if (url.toString() != 'stdin') { - _includedFiles.add(url.toString()); - } - } + if (!(_asNodeSass && url.toString() == 'stdin')) _includedUrls.add(url); } var module = await _execute(importer, node); - return EvaluateResult(_combineCss(module), _includedFiles); + return EvaluateResult(_combineCss(module), _includedUrls); }); } @@ -1577,13 +1567,17 @@ class _EvaluateVisitor tuple.item1, tuple.item2, originalUrl: tuple.item3, quiet: _quietDeps && isDependency); if (stylesheet != null) { + _includedUrls.add(tuple.item2); return _LoadedStylesheet(stylesheet, importer: tuple.item1, isDependency: isDependency); } } } else { var result = await _importLikeNode(url, forImport); - if (result != null) return result; + if (result != null) { + result.stylesheet.span.sourceUrl.andThen(_includedUrls.add); + return result; + } } if (url.startsWith('package:') && isNode) { @@ -1629,8 +1623,6 @@ class _EvaluateVisitor var contents = result.item1; var url = result.item2; - _includedFiles.add(url.startsWith('file:') ? p.fromUri(url) : url); - return _LoadedStylesheet( Stylesheet.parse(contents, url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, @@ -3316,14 +3308,10 @@ class EvaluateResult { /// The CSS syntax tree. final CssStylesheet stylesheet; - /// The set that will eventually populate the JS API's - /// `result.stats.includedFiles` field. - /// - /// For filesystem imports, this contains the import path. For all other - /// imports, it contains the URL passed to the `@import`. - final Set includedFiles; + /// The canonical URLs of all stylesheets loaded during compilation. + final Set includedUrls; - EvaluateResult(this.stylesheet, this.includedFiles); + EvaluateResult(this.stylesheet, this.includedUrls); } /// The result of evaluating arguments to a function or mixin. diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 34296235..5b1d6316 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: b2321a00031707d2df699e6888a334deba39995d +// Checksum: 5e03597bf64a5e03b483e64e4d35e4e86288bc10 // // ignore_for_file: unused_import @@ -224,12 +224,8 @@ class _EvaluateVisitor /// Whether we're currently building the output of a `@keyframes` rule. var _inKeyframes = false; - /// The set that will eventually populate the JS API's - /// `result.stats.includedFiles` field. - /// - /// For filesystem imports, this contains the import path. For all other - /// imports, it contains the URL passed to the `@import`. - final _includedFiles = {}; + /// The canonical URLs of all stylesheets loaded during compilation. + final _includedUrls = {}; /// A map from canonical URLs for modules (or imported files) that are /// currently being evaluated to AST nodes whose spans indicate the original @@ -513,18 +509,12 @@ class _EvaluateVisitor var url = node.span.sourceUrl; if (url != null) { _activeModules[url] = null; - if (_asNodeSass) { - if (url.scheme == 'file') { - _includedFiles.add(p.fromUri(url)); - } else if (url.toString() != 'stdin') { - _includedFiles.add(url.toString()); - } - } + if (!(_asNodeSass && url.toString() == 'stdin')) _includedUrls.add(url); } var module = _execute(importer, node); - return EvaluateResult(_combineCss(module), _includedFiles); + return EvaluateResult(_combineCss(module), _includedUrls); }); } @@ -1575,13 +1565,17 @@ class _EvaluateVisitor var stylesheet = importCache.importCanonical(tuple.item1, tuple.item2, originalUrl: tuple.item3, quiet: _quietDeps && isDependency); if (stylesheet != null) { + _includedUrls.add(tuple.item2); return _LoadedStylesheet(stylesheet, importer: tuple.item1, isDependency: isDependency); } } } else { var result = _importLikeNode(url, forImport); - if (result != null) return result; + if (result != null) { + result.stylesheet.span.sourceUrl.andThen(_includedUrls.add); + return result; + } } if (url.startsWith('package:') && isNode) { @@ -1626,8 +1620,6 @@ class _EvaluateVisitor var contents = result.item1; var url = result.item2; - _includedFiles.add(url.startsWith('file:') ? p.fromUri(url) : url); - return _LoadedStylesheet( Stylesheet.parse(contents, url.startsWith('file') ? Syntax.forPath(url) : Syntax.scss, diff --git a/pubspec.yaml b/pubspec.yaml index 140e349a..eada8ad4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.35.2 +version: 1.36.0 description: A Sass implementation in Dart. author: Sass Team homepage: https://github.com/sass/dart-sass diff --git a/test/cli/shared/source_maps.dart b/test/cli/shared/source_maps.dart index 09d9a8b8..647f6aa1 100644 --- a/test/cli/shared/source_maps.dart +++ b/test/cli/shared/source_maps.dart @@ -5,7 +5,6 @@ import 'dart:convert'; import 'package:path/path.dart' as p; -import 'package:source_maps/source_maps.dart'; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test_process/test_process.dart'; @@ -35,9 +34,9 @@ void sharedTests(Future runSass(Iterable arguments)) { }); test("contains mappings", () { - late SingleMapping sourceMap; - sass.compileString("a {b: 1 + 2}", sourceMap: (map) => sourceMap = map); - expect(map, containsPair("mappings", sourceMap.toJson()["mappings"])); + var result = sass.compileStringToResult("a {b: 1 + 2}"); + expect(map, + containsPair("mappings", result.sourceMap!.toJson()["mappings"])); }); }); @@ -288,9 +287,9 @@ void sharedTests(Future runSass(Iterable arguments)) { }); test("contains mappings in the generated CSS", () { - late SingleMapping sourceMap; - sass.compileString("a {b: 1 + 2}", sourceMap: (map) => sourceMap = map); - expect(map, containsPair("mappings", sourceMap.toJson()["mappings"])); + var result = sass.compileStringToResult("a {b: 1 + 2}"); + expect(map, + containsPair("mappings", result.sourceMap!.toJson()["mappings"])); }); test("refers to the source file", () { diff --git a/test/dart_api/importer_test.dart b/test/dart_api/importer_test.dart index efbebb06..641c59d1 100644 --- a/test/dart_api/importer_test.dart +++ b/test/dart_api/importer_test.dart @@ -6,7 +6,6 @@ import 'dart:convert'; -import 'package:source_maps/source_maps.dart'; import 'package:test/test.dart'; import 'package:sass/sass.dart'; @@ -115,33 +114,27 @@ void main() { }); test("uses an importer's source map URL", () { - late SingleMapping map; - compileString('@import "orange";', - importers: [ - TestImporter((url) => Uri.parse("u:$url"), (url) { - var color = url.path; - return ImporterResult('.$color {color: $color}', - sourceMapUrl: Uri.parse("u:blue"), indented: false); - }) - ], - sourceMap: (map_) => map = map_); + var result = compileStringToResult('@import "orange";', importers: [ + TestImporter((url) => Uri.parse("u:$url"), (url) { + var color = url.path; + return ImporterResult('.$color {color: $color}', + sourceMapUrl: Uri.parse("u:blue"), indented: false); + }) + ]); - expect(map.urls, contains("u:blue")); + expect(result.sourceMap!.urls, contains("u:blue")); }); test("uses a data: source map URL if the importer doesn't provide one", () { - late SingleMapping map; - compileString('@import "orange";', - importers: [ - TestImporter((url) => Uri.parse("u:$url"), (url) { - var color = url.path; - return ImporterResult('.$color {color: $color}', indented: false); - }) - ], - sourceMap: (map_) => map = map_); + var result = compileStringToResult('@import "orange";', importers: [ + TestImporter((url) => Uri.parse("u:$url"), (url) { + var color = url.path; + return ImporterResult('.$color {color: $color}', indented: false); + }) + ]); expect( - map.urls, + result.sourceMap!.urls, contains(Uri.dataFromString(".orange {color: orange}", encoding: utf8) .toString())); }); diff --git a/test/dart_api_test.dart b/test/dart_api_test.dart index ec7e8644..93638ae3 100644 --- a/test/dart_api_test.dart +++ b/test/dart_api_test.dart @@ -226,6 +226,80 @@ a { }); }); + group("includedUrls", () { + group("contains the entrypoint's URL", () { + group("in compileStringToResult()", () { + test("if it's given", () { + var result = compileStringToResult("a {b: c}", url: "source.scss"); + expect(result.includedUrls, equals([Uri.parse("source.scss")])); + }); + + test("unless it's not given", () { + var result = compileStringToResult("a {b: c}"); + expect(result.includedUrls, isEmpty); + }); + }); + + test("in compileToResult()", () async { + await d.file("input.scss", "a {b: c};").create(); + var result = compileToResult(d.path('input.scss')); + expect(result.includedUrls, equals([p.toUri(d.path('input.scss'))])); + }); + }); + + test("contains a URL loaded via @import", () async { + await d.file("_other.scss", "a {b: c}").create(); + await d.file("input.scss", "@import 'other';").create(); + var result = compileToResult(d.path('input.scss')); + expect(result.includedUrls, contains(p.toUri(d.path('_other.scss')))); + }); + + test("contains a URL loaded via @use", () async { + await d.file("_other.scss", "a {b: c}").create(); + await d.file("input.scss", "@use 'other';").create(); + var result = compileToResult(d.path('input.scss')); + expect(result.includedUrls, contains(p.toUri(d.path('_other.scss')))); + }); + + test("contains a URL loaded via @forward", () async { + await d.file("_other.scss", "a {b: c}").create(); + await d.file("input.scss", "@forward 'other';").create(); + var result = compileToResult(d.path('input.scss')); + expect(result.includedUrls, contains(p.toUri(d.path('_other.scss')))); + }); + + test("contains a URL loaded via @meta.load-css()", () async { + await d.file("_other.scss", "a {b: c}").create(); + await d.file("input.scss", """ + @use 'sass:meta'; + @include meta.load-css('other'); + """).create(); + var result = compileToResult(d.path('input.scss')); + expect(result.includedUrls, contains(p.toUri(d.path('_other.scss')))); + }); + + test("contains a URL loaded via a chain of loads", () async { + await d.file("_jupiter.scss", "a {b: c}").create(); + await d.file("_mars.scss", "@forward 'jupiter';").create(); + await d.file("_earth.scss", "@import 'mars';").create(); + await d.file("_venus.scss", "@use 'earth';").create(); + await d.file("mercury.scss", """ + @use 'sass:meta'; + @include meta.load-css('venus'); + """).create(); + var result = compileToResult(d.path('mercury.scss')); + expect( + result.includedUrls, + unorderedEquals([ + p.toUri(d.path('mercury.scss')), + p.toUri(d.path('_venus.scss')), + p.toUri(d.path('_earth.scss')), + p.toUri(d.path('_mars.scss')), + p.toUri(d.path('_jupiter.scss')) + ])); + }); + }); + // Regression test for #1318 test("meta.load-module() doesn't have a race condition", () async { await d.file("other.scss", '/**//**/').create(); diff --git a/tool/grind/synchronize.dart b/tool/grind/synchronize.dart index 452e16df..9cb3c409 100644 --- a/tool/grind/synchronize.dart +++ b/tool/grind/synchronize.dart @@ -27,7 +27,7 @@ final sources = const { /// Classes that are defined in the async version of a file and used as-is in /// the sync version, and thus should not be copied over. -final _sharedClasses = const ['EvaluateResult', 'CompileResult']; +final _sharedClasses = const ['EvaluateResult']; /// This is how we support both synchronous and asynchronous compilation modes. /// @@ -97,7 +97,6 @@ class _Visitor extends RecursiveAstVisitor { _buffer.writeln(); } else if (p.basename(path) == 'async_compile.dart') { _buffer.writeln(); - _buffer.writeln("import 'async_compile.dart';"); _buffer.writeln("export 'async_compile.dart';"); _buffer.writeln(); }