diff --git a/CHANGELOG.md b/CHANGELOG.md index ae92aca14..adfce2e9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.76.0 + +* Throw errors for misplaced statements in keyframe blocks. + +* Mixins and functions whose names begin with `--` are now deprecated for + forwards-compatibility with the in-progress CSS functions and mixins spec. + This deprecation is named `css-function-mixin`. + ## 1.75.0 * Fix a bug in which stylesheet canonicalization could be cached incorrectly diff --git a/README.md b/README.md index 844cdf0fc..fbc545065 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ A [Dart][dart] implementation of [Sass][sass]. **Sass makes CSS fun**. * [Compatibility Policy](#compatibility-policy) * [Browser Compatibility](#browser-compatibility) * [Node.js Compatibility](#nodejs-compatibility) + * [Invalid CSS](#invalid-css) * [Embedded Dart Sass](#embedded-dart-sass) * [Usage](#usage) * [Behavioral Differences from Ruby Sass](#behavioral-differences-from-ruby-sass) @@ -405,6 +406,18 @@ considers itself free to break support if necessary. [the Node.js release page]: https://nodejs.org/en/about/previous-releases +### Invalid CSS + +Changes to the behavior of Sass stylesheets that produce invalid CSS output are +_not_ considered breaking changes. Such changes are almost always necessary when +adding support for new CSS features, and delaying all such features until a new +major version would be unduly burdensome for most users. + +For example, when Sass began parsing `calc()` expressions, the invalid +expression `calc(1 +)` became a Sass error where before it was passed through +as-is. This was not considered a breaking change, because `calc(1 +)` was never +valid CSS to begin with. + ## Embedded Dart Sass Dart Sass includes an implementation of the compiler side of the [Embedded Sass diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 576094cb4..6d6e4fa8c 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -11,10 +11,12 @@ import 'package:path/path.dart' as p; import 'ast/sass.dart'; import 'deprecation.dart'; import 'importer.dart'; +import 'importer/canonicalize_context.dart'; import 'importer/no_op.dart'; import 'importer/utils.dart'; import 'io.dart'; import 'logger.dart'; +import 'util/map.dart'; import 'util/nullable.dart'; import 'utils.dart'; @@ -43,30 +45,28 @@ final class AsyncImportCache { /// The `forImport` in each key is true when this canonicalization is for an /// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule. /// - /// This cache isn't used for relative imports, because they depend on the - /// specific base importer. That's stored separately in - /// [_relativeCanonicalizeCache]. + /// This cache covers loads that go through the entire chain of [_importers], + /// but it doesn't cover individual loads or loads in which any importer + /// accesses `containingUrl`. See also [_perImporterCanonicalizeCache]. final _canonicalizeCache = <(Uri, {bool forImport}), AsyncCanonicalizeResult?>{}; - /// The canonicalized URLs for each non-canonical URL that's resolved using a - /// relative importer. + /// Like [_canonicalizeCache] but also includes the specific importer in the + /// key. /// - /// The map's keys have four parts: + /// This is used to cache both relative imports from the base importer and + /// individual importer results in the case where some other component of the + /// importer chain isn't cacheable. + final _perImporterCanonicalizeCache = + <(AsyncImporter, Uri, {bool forImport}), AsyncCanonicalizeResult?>{}; + + /// A map from the keys in [_perImporterCanonicalizeCache] that are generated + /// for relative URL loads agains the base importer to the original relative + /// URLs what were loaded. /// - /// 1. The URL passed to [canonicalize] (the same as in [_canonicalizeCache]). - /// 2. Whether the canonicalization is for an `@import` rule. - /// 3. The `baseImporter` passed to [canonicalize]. - /// 4. The `baseUrl` passed to [canonicalize]. - /// - /// The map's values are the same as the return value of [canonicalize]. - final _relativeCanonicalizeCache = <( - Uri, { - bool forImport, - AsyncImporter baseImporter, - Uri? baseUrl - }), - AsyncCanonicalizeResult?>{}; + /// This is used to invalidate the cache when files are changed. + final _nonCanonicalRelativeUrls = + <(AsyncImporter, Uri, {bool forImport}), Uri>{}; /// The parsed stylesheets for each canonicalized import URL. final _importCache = {}; @@ -154,18 +154,17 @@ final class AsyncImportCache { } if (baseImporter != null && url.scheme == '') { - var relativeResult = await putIfAbsentAsync(_relativeCanonicalizeCache, ( - url, - forImport: forImport, - baseImporter: baseImporter, - baseUrl: baseUrl - ), () async { - var (result, cacheable) = await _canonicalize( - baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport); + var resolvedUrl = baseUrl?.resolveUri(url) ?? url; + var key = (baseImporter, resolvedUrl, forImport: forImport); + var relativeResult = + await putIfAbsentAsync(_perImporterCanonicalizeCache, key, () async { + var (result, cacheable) = + await _canonicalize(baseImporter, resolvedUrl, baseUrl, forImport); assert( cacheable, "Relative loads should always be cacheable because they never " "provide access to the containing URL."); + if (baseUrl != null) _nonCanonicalRelativeUrls[key] = url; return result; }); if (relativeResult != null) return relativeResult; @@ -181,17 +180,41 @@ final class AsyncImportCache { // `canonicalize()` calls we've attempted are cacheable. Only if they are do // we store the result in the cache. var cacheable = true; - for (var importer in _importers) { + for (var i = 0; i < _importers.length; i++) { + var importer = _importers[i]; + var perImporterKey = (importer, url, forImport: forImport); + switch (_perImporterCanonicalizeCache.getOption(perImporterKey)) { + case (var result?,): + return result; + case (null,): + continue; + } + switch (await _canonicalize(importer, url, baseUrl, forImport)) { case (var result?, true) when cacheable: _canonicalizeCache[key] = result; return result; - case (var result?, _): - return result; - - case (_, false): - cacheable = false; + case (var result, true) when !cacheable: + _perImporterCanonicalizeCache[perImporterKey] = result; + if (result != null) return result; + + case (var result, false): + if (cacheable) { + // If this is the first uncacheable result, add all previous results + // to the per-importer cache so we don't have to re-run them for + // future uses of this importer. + for (var j = 0; j < i; j++) { + _perImporterCanonicalizeCache[( + _importers[j], + url, + forImport: forImport + )] = null; + } + cacheable = false; + } + + if (result != null) return result; } } @@ -206,18 +229,17 @@ final class AsyncImportCache { /// that result is cacheable at all. Future<(AsyncCanonicalizeResult?, bool cacheable)> _canonicalize( AsyncImporter importer, Uri url, Uri? baseUrl, bool forImport) async { - var canonicalize = forImport - ? () => inImportRule(() => importer.canonicalize(url)) - : () => importer.canonicalize(url); - var passContainingUrl = baseUrl != null && (url.scheme == '' || await importer.isNonCanonicalScheme(url.scheme)); - var result = await withContainingUrl( - passContainingUrl ? baseUrl : null, canonicalize); - // TODO(sass/dart-sass#2208): Determine whether the containing URL was - // _actually_ accessed rather than assuming it was. - var cacheable = !passContainingUrl || importer is FilesystemImporter; + var canonicalizeContext = + CanonicalizeContext(passContainingUrl ? baseUrl : null, forImport); + + var result = await withCanonicalizeContext( + canonicalizeContext, () => importer.canonicalize(url)); + + var cacheable = + !passContainingUrl || !canonicalizeContext.wasContainingUrlAccessed; if (result == null) return (null, cacheable); @@ -315,7 +337,7 @@ final class AsyncImportCache { Uri sourceMapUrl(Uri canonicalUrl) => _resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl; - /// Clears the cached canonical version of the given [url]. + /// Clears the cached canonical version of the given non-canonical [url]. /// /// Has no effect if the canonical version of [url] has not been cached. /// @@ -324,7 +346,8 @@ final class AsyncImportCache { void clearCanonicalize(Uri url) { _canonicalizeCache.remove((url, forImport: false)); _canonicalizeCache.remove((url, forImport: true)); - _relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url); + _perImporterCanonicalizeCache.removeWhere( + (key, _) => key.$2 == url || _nonCanonicalRelativeUrls[key] == url); } /// Clears the cached parse tree for the stylesheet with the given diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index a7412e2ce..b13180e10 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -74,6 +74,10 @@ enum Deprecation { description: 'Using the current working directory as an implicit load path.'), + cssFunctionMixin('css-function-mixin', + deprecatedIn: '1.76.0', + description: 'Function and mixin names beginning with --.'), + @Deprecated('This deprecation name was never actually used.') calcInterp('calc-interp', deprecatedIn: null), diff --git a/lib/src/embedded/importer/file.dart b/lib/src/embedded/importer/file.dart index 57d97ddf9..7c4d9975c 100644 --- a/lib/src/embedded/importer/file.dart +++ b/lib/src/embedded/importer/file.dart @@ -21,10 +21,12 @@ final class FileImporter extends ImporterBase { ..importerId = _importerId ..url = url.toString() ..fromImport = fromImport; - if (containingUrl case var containingUrl?) { + if (canonicalizeContext.containingUrlWithoutMarking + case var containingUrl?) { request.containingUrl = containingUrl.toString(); } var response = dispatcher.sendFileImportRequest(request); + if (!response.containingUrlUnused) canonicalizeContext.containingUrl; switch (response.whichResult()) { case InboundMessage_FileImportResponse_Result.fileUrl: diff --git a/lib/src/embedded/importer/host.dart b/lib/src/embedded/importer/host.dart index 25245721b..e5342dc31 100644 --- a/lib/src/embedded/importer/host.dart +++ b/lib/src/embedded/importer/host.dart @@ -35,10 +35,12 @@ final class HostImporter extends ImporterBase { ..importerId = _importerId ..url = url.toString() ..fromImport = fromImport; - if (containingUrl case var containingUrl?) { + if (canonicalizeContext.containingUrlWithoutMarking + case var containingUrl?) { request.containingUrl = containingUrl.toString(); } var response = dispatcher.sendCanonicalizeRequest(request); + if (!response.containingUrlUnused) canonicalizeContext.containingUrl; return switch (response.whichResult()) { InboundMessage_CanonicalizeResponse_Result.url => diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 6204971e4..9590b0e5a 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: 37dd173d676ec6cf201a25b3cca9ac81d92b1433 +// Checksum: 4362e28e5cd425786c235d2a6a2bb60539403799 // // ignore_for_file: unused_import @@ -18,10 +18,12 @@ import 'package:path/path.dart' as p; import 'ast/sass.dart'; import 'deprecation.dart'; import 'importer.dart'; +import 'importer/canonicalize_context.dart'; import 'importer/no_op.dart'; import 'importer/utils.dart'; import 'io.dart'; import 'logger.dart'; +import 'util/map.dart'; import 'util/nullable.dart'; import 'utils.dart'; @@ -46,29 +48,26 @@ final class ImportCache { /// The `forImport` in each key is true when this canonicalization is for an /// `@import` rule. Otherwise, it's for a `@use` or `@forward` rule. /// - /// This cache isn't used for relative imports, because they depend on the - /// specific base importer. That's stored separately in - /// [_relativeCanonicalizeCache]. + /// This cache covers loads that go through the entire chain of [_importers], + /// but it doesn't cover individual loads or loads in which any importer + /// accesses `containingUrl`. See also [_perImporterCanonicalizeCache]. final _canonicalizeCache = <(Uri, {bool forImport}), CanonicalizeResult?>{}; - /// The canonicalized URLs for each non-canonical URL that's resolved using a - /// relative importer. + /// Like [_canonicalizeCache] but also includes the specific importer in the + /// key. /// - /// The map's keys have four parts: + /// This is used to cache both relative imports from the base importer and + /// individual importer results in the case where some other component of the + /// importer chain isn't cacheable. + final _perImporterCanonicalizeCache = + <(Importer, Uri, {bool forImport}), CanonicalizeResult?>{}; + + /// A map from the keys in [_perImporterCanonicalizeCache] that are generated + /// for relative URL loads agains the base importer to the original relative + /// URLs what were loaded. /// - /// 1. The URL passed to [canonicalize] (the same as in [_canonicalizeCache]). - /// 2. Whether the canonicalization is for an `@import` rule. - /// 3. The `baseImporter` passed to [canonicalize]. - /// 4. The `baseUrl` passed to [canonicalize]. - /// - /// The map's values are the same as the return value of [canonicalize]. - final _relativeCanonicalizeCache = <( - Uri, { - bool forImport, - Importer baseImporter, - Uri? baseUrl - }), - CanonicalizeResult?>{}; + /// This is used to invalidate the cache when files are changed. + final _nonCanonicalRelativeUrls = <(Importer, Uri, {bool forImport}), Uri>{}; /// The parsed stylesheets for each canonicalized import URL. final _importCache = {}; @@ -154,18 +153,16 @@ final class ImportCache { } if (baseImporter != null && url.scheme == '') { - var relativeResult = _relativeCanonicalizeCache.putIfAbsent(( - url, - forImport: forImport, - baseImporter: baseImporter, - baseUrl: baseUrl - ), () { - var (result, cacheable) = _canonicalize( - baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport); + var resolvedUrl = baseUrl?.resolveUri(url) ?? url; + var key = (baseImporter, resolvedUrl, forImport: forImport); + var relativeResult = _perImporterCanonicalizeCache.putIfAbsent(key, () { + var (result, cacheable) = + _canonicalize(baseImporter, resolvedUrl, baseUrl, forImport); assert( cacheable, "Relative loads should always be cacheable because they never " "provide access to the containing URL."); + if (baseUrl != null) _nonCanonicalRelativeUrls[key] = url; return result; }); if (relativeResult != null) return relativeResult; @@ -181,17 +178,41 @@ final class ImportCache { // `canonicalize()` calls we've attempted are cacheable. Only if they are do // we store the result in the cache. var cacheable = true; - for (var importer in _importers) { + for (var i = 0; i < _importers.length; i++) { + var importer = _importers[i]; + var perImporterKey = (importer, url, forImport: forImport); + switch (_perImporterCanonicalizeCache.getOption(perImporterKey)) { + case (var result?,): + return result; + case (null,): + continue; + } + switch (_canonicalize(importer, url, baseUrl, forImport)) { case (var result?, true) when cacheable: _canonicalizeCache[key] = result; return result; - case (var result?, _): - return result; - - case (_, false): - cacheable = false; + case (var result, true) when !cacheable: + _perImporterCanonicalizeCache[perImporterKey] = result; + if (result != null) return result; + + case (var result, false): + if (cacheable) { + // If this is the first uncacheable result, add all previous results + // to the per-importer cache so we don't have to re-run them for + // future uses of this importer. + for (var j = 0; j < i; j++) { + _perImporterCanonicalizeCache[( + _importers[j], + url, + forImport: forImport + )] = null; + } + cacheable = false; + } + + if (result != null) return result; } } @@ -206,18 +227,17 @@ final class ImportCache { /// that result is cacheable at all. (CanonicalizeResult?, bool cacheable) _canonicalize( Importer importer, Uri url, Uri? baseUrl, bool forImport) { - var canonicalize = forImport - ? () => inImportRule(() => importer.canonicalize(url)) - : () => importer.canonicalize(url); - var passContainingUrl = baseUrl != null && (url.scheme == '' || importer.isNonCanonicalScheme(url.scheme)); - var result = - withContainingUrl(passContainingUrl ? baseUrl : null, canonicalize); - // TODO(sass/dart-sass#2208): Determine whether the containing URL was - // _actually_ accessed rather than assuming it was. - var cacheable = !passContainingUrl || importer is FilesystemImporter; + var canonicalizeContext = + CanonicalizeContext(passContainingUrl ? baseUrl : null, forImport); + + var result = withCanonicalizeContext( + canonicalizeContext, () => importer.canonicalize(url)); + + var cacheable = + !passContainingUrl || !canonicalizeContext.wasContainingUrlAccessed; if (result == null) return (null, cacheable); @@ -312,7 +332,7 @@ final class ImportCache { Uri sourceMapUrl(Uri canonicalUrl) => _resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl; - /// Clears the cached canonical version of the given [url]. + /// Clears the cached canonical version of the given non-canonical [url]. /// /// Has no effect if the canonical version of [url] has not been cached. /// @@ -321,7 +341,8 @@ final class ImportCache { void clearCanonicalize(Uri url) { _canonicalizeCache.remove((url, forImport: false)); _canonicalizeCache.remove((url, forImport: true)); - _relativeCanonicalizeCache.removeWhere((key, _) => key.$1 == url); + _perImporterCanonicalizeCache.removeWhere( + (key, _) => key.$2 == url || _nonCanonicalRelativeUrls[key] == url); } /// Clears the cached parse tree for the stylesheet with the given diff --git a/lib/src/importer/async.dart b/lib/src/importer/async.dart index d7d6951fe..d777d84f5 100644 --- a/lib/src/importer/async.dart +++ b/lib/src/importer/async.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:meta/meta.dart'; +import 'canonicalize_context.dart'; import 'result.dart'; import 'utils.dart' as utils; @@ -54,7 +55,19 @@ abstract class AsyncImporter { /// Outside of that context, its value is undefined and subject to change. @protected @nonVirtual - Uri? get containingUrl => utils.containingUrl; + Uri? get containingUrl => utils.canonicalizeContext.containingUrl; + + /// The canonicalize context of the stylesheet that caused the current + /// [canonicalize] invocation. + /// + /// Subclasses should only access this from within calls to [canonicalize]. + /// Outside of that context, its value is undefined and subject to change. + /// + /// @nodoc + @internal + @protected + @nonVirtual + CanonicalizeContext get canonicalizeContext => utils.canonicalizeContext; /// If [url] is recognized by this importer, returns its canonical format. /// diff --git a/lib/src/importer/canonicalize_context.dart b/lib/src/importer/canonicalize_context.dart new file mode 100644 index 000000000..e28e69e8d --- /dev/null +++ b/lib/src/importer/canonicalize_context.dart @@ -0,0 +1,47 @@ +// Copyright 2024 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 'dart:async'; + +import 'package:meta/meta.dart'; + +/// Contextual information used by importers' `canonicalize` method. +@internal +final class CanonicalizeContext { + /// Whether the Sass compiler is currently evaluating an `@import` rule. + bool get fromImport => _fromImport; + bool _fromImport; + + /// The URL of the stylesheet that contains the current load. + Uri? get containingUrl { + _wasContainingUrlAccessed = true; + return _containingUrl; + } + + final Uri? _containingUrl; + + /// Returns the same value as [containingUrl], but doesn't mark it accessed. + Uri? get containingUrlWithoutMarking => _containingUrl; + + /// Whether [containingUrl] has been accessed. + /// + /// This is used to determine whether canonicalize result is cacheable. + bool get wasContainingUrlAccessed => _wasContainingUrlAccessed; + var _wasContainingUrlAccessed = false; + + /// Runs [callback] in a context with specificed [fromImport]. + T withFromImport(bool fromImport, T callback()) { + assert(Zone.current[#_canonicalizeContext] == this); + + var oldFromImport = _fromImport; + _fromImport = fromImport; + try { + return callback(); + } finally { + _fromImport = oldFromImport; + } + } + + CanonicalizeContext(this._containingUrl, this._fromImport); +} diff --git a/lib/src/importer/js_to_dart/async.dart b/lib/src/importer/js_to_dart/async.dart index 11ffbd735..c6d26cbe2 100644 --- a/lib/src/importer/js_to_dart/async.dart +++ b/lib/src/importer/js_to_dart/async.dart @@ -13,6 +13,7 @@ import '../../js/url.dart'; import '../../js/utils.dart'; import '../../util/nullable.dart'; import '../async.dart'; +import '../canonicalize_context.dart'; import '../result.dart'; import 'utils.dart'; @@ -38,11 +39,8 @@ final class JSToDartAsyncImporter extends AsyncImporter { } FutureOr canonicalize(Uri url) async { - var result = wrapJSExceptions(() => _canonicalize( - url.toString(), - CanonicalizeContext( - fromImport: fromImport, - containingUrl: containingUrl.andThen(dartToJSUrl)))); + var result = wrapJSExceptions( + () => _canonicalize(url.toString(), canonicalizeContext)); if (isPromise(result)) result = await promiseToFuture(result as Promise); if (result == null) return null; diff --git a/lib/src/importer/js_to_dart/async_file.dart b/lib/src/importer/js_to_dart/async_file.dart index 7be4b9461..95b2af908 100644 --- a/lib/src/importer/js_to_dart/async_file.dart +++ b/lib/src/importer/js_to_dart/async_file.dart @@ -8,11 +8,10 @@ import 'package:cli_pkg/js.dart'; import 'package:node_interop/js.dart'; import 'package:node_interop/util.dart'; -import '../../js/importer.dart'; import '../../js/url.dart'; import '../../js/utils.dart'; -import '../../util/nullable.dart'; import '../async.dart'; +import '../canonicalize_context.dart'; import '../filesystem.dart'; import '../result.dart'; import '../utils.dart'; @@ -28,11 +27,8 @@ final class JSToDartAsyncFileImporter extends AsyncImporter { FutureOr canonicalize(Uri url) async { if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); - var result = wrapJSExceptions(() => _findFileUrl( - url.toString(), - CanonicalizeContext( - fromImport: fromImport, - containingUrl: containingUrl.andThen(dartToJSUrl)))); + var result = wrapJSExceptions( + () => _findFileUrl(url.toString(), canonicalizeContext)); if (isPromise(result)) result = await promiseToFuture(result as Promise); if (result == null) return null; if (!isJSUrl(result)) { diff --git a/lib/src/importer/js_to_dart/file.dart b/lib/src/importer/js_to_dart/file.dart index e3302f881..555c9df16 100644 --- a/lib/src/importer/js_to_dart/file.dart +++ b/lib/src/importer/js_to_dart/file.dart @@ -6,10 +6,9 @@ import 'package:cli_pkg/js.dart'; import 'package:node_interop/js.dart'; import '../../importer.dart'; -import '../../js/importer.dart'; import '../../js/url.dart'; import '../../js/utils.dart'; -import '../../util/nullable.dart'; +import '../canonicalize_context.dart'; import '../utils.dart'; /// A wrapper for a potentially-asynchronous JS API file importer that exposes @@ -23,11 +22,8 @@ final class JSToDartFileImporter extends Importer { Uri? canonicalize(Uri url) { if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); - var result = wrapJSExceptions(() => _findFileUrl( - url.toString(), - CanonicalizeContext( - fromImport: fromImport, - containingUrl: containingUrl.andThen(dartToJSUrl)))); + var result = wrapJSExceptions( + () => _findFileUrl(url.toString(), canonicalizeContext)); if (result == null) return null; if (isPromise(result)) { diff --git a/lib/src/importer/js_to_dart/sync.dart b/lib/src/importer/js_to_dart/sync.dart index f69dafb35..06b91310a 100644 --- a/lib/src/importer/js_to_dart/sync.dart +++ b/lib/src/importer/js_to_dart/sync.dart @@ -10,6 +10,7 @@ import '../../js/importer.dart'; import '../../js/url.dart'; import '../../js/utils.dart'; import '../../util/nullable.dart'; +import '../canonicalize_context.dart'; import 'utils.dart'; /// A wrapper for a synchronous JS API importer that exposes it as a Dart @@ -34,11 +35,8 @@ final class JSToDartImporter extends Importer { } Uri? canonicalize(Uri url) { - var result = wrapJSExceptions(() => _canonicalize( - url.toString(), - CanonicalizeContext( - fromImport: fromImport, - containingUrl: containingUrl.andThen(dartToJSUrl)))); + var result = wrapJSExceptions( + () => _canonicalize(url.toString(), canonicalizeContext)); if (result == null) return null; if (isJSUrl(result)) return jsToDartUrl(result as JSUrl); diff --git a/lib/src/importer/utils.dart b/lib/src/importer/utils.dart index a68ae6f5e..4c5c8106c 100644 --- a/lib/src/importer/utils.dart +++ b/lib/src/importer/utils.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:path/path.dart' as p; import '../io.dart'; +import './canonicalize_context.dart'; /// Whether the Sass compiler is currently evaluating an `@import` rule. /// @@ -15,30 +16,35 @@ import '../io.dart'; /// canonicalization should be identical for `@import` and `@use` rules. It's /// admittedly hacky to set this globally, but `@import` will eventually be /// removed, at which point we can delete this and have one consistent behavior. -bool get fromImport => Zone.current[#_inImportRule] as bool? ?? false; - -/// The URL of the stylesheet that contains the current load. -Uri? get containingUrl => switch (Zone.current[#_containingUrl]) { +bool get fromImport => + ((Zone.current[#_canonicalizeContext] as CanonicalizeContext?) + ?.fromImport ?? + false); + +/// The CanonicalizeContext of the current load. +CanonicalizeContext get canonicalizeContext => + switch (Zone.current[#_canonicalizeContext]) { null => throw StateError( - "containingUrl may only be accessed within a call to canonicalize()."), - #_none => null, - Uri url => url, + "canonicalizeContext may only be accessed within a call to canonicalize()."), + CanonicalizeContext context => context, var value => throw StateError( - "Unexpected Zone.current[#_containingUrl] value $value.") + "Unexpected Zone.current[#_canonicalizeContext] value $value.") }; /// Runs [callback] in a context where [fromImport] returns `true` and /// [resolveImportPath] uses `@import` semantics rather than `@use` semantics. T inImportRule(T callback()) => - runZoned(callback, zoneValues: {#_inImportRule: true}); + switch (Zone.current[#_canonicalizeContext]) { + null => runZoned(callback, + zoneValues: {#_canonicalizeContext: CanonicalizeContext(null, true)}), + CanonicalizeContext context => context.withFromImport(true, callback), + var value => throw StateError( + "Unexpected Zone.current[#_canonicalizeContext] value $value.") + }; -/// Runs [callback] in a context where [containingUrl] returns [url]. -/// -/// If [when] is `false`, runs [callback] without setting [containingUrl]. -T withContainingUrl(Uri? url, T callback()) => - // Use #_none as a sentinel value so we can distinguish a containing URL - // that's set to null from one that's unset at all. - runZoned(callback, zoneValues: {#_containingUrl: url ?? #_none}); +/// Runs [callback] in the given context. +T withCanonicalizeContext(CanonicalizeContext? context, T callback()) => + runZoned(callback, zoneValues: {#_canonicalizeContext: context}); /// Resolves an imported path using the same logic as the filesystem importer. /// diff --git a/lib/src/js.dart b/lib/src/js.dart index 79bf90180..dc0384bc4 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -7,6 +7,7 @@ import 'package:js/js_util.dart'; import 'js/exception.dart'; import 'js/deprecations.dart'; import 'js/exports.dart'; +import 'js/importer/canonicalize_context.dart'; import 'js/compile.dart'; import 'js/compiler.dart'; import 'js/legacy.dart'; @@ -64,6 +65,7 @@ void main() { "dart2js\t${const String.fromEnvironment('dart-version')}\t" "(Dart Compiler)\t[Dart]"; + updateCanonicalizeContextPrototype(); updateSourceSpanPrototype(); // Legacy API diff --git a/lib/src/js/importer.dart b/lib/src/js/importer.dart index 0469737ee..09ffcd665 100644 --- a/lib/src/js/importer.dart +++ b/lib/src/js/importer.dart @@ -4,6 +4,7 @@ import 'package:js/js.dart'; +import '../importer/canonicalize_context.dart'; import 'url.dart'; @JS() @@ -15,15 +16,6 @@ class JSImporter { external Object? get nonCanonicalScheme; } -@JS() -@anonymous -class CanonicalizeContext { - external bool get fromImport; - external JSUrl? get containingUrl; - - external factory CanonicalizeContext({bool fromImport, JSUrl? containingUrl}); -} - @JS() @anonymous class JSImporterResult { diff --git a/lib/src/js/importer/canonicalize_context.dart b/lib/src/js/importer/canonicalize_context.dart new file mode 100644 index 000000000..412f21ce8 --- /dev/null +++ b/lib/src/js/importer/canonicalize_context.dart @@ -0,0 +1,16 @@ +// Copyright 2014 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 '../../importer/canonicalize_context.dart'; +import '../../util/nullable.dart'; +import '../reflection.dart'; +import '../utils.dart'; + +/// Adds JS members to Dart's `CanonicalizeContext` class. +void updateCanonicalizeContextPrototype() => + getJSClass(CanonicalizeContext(null, false)).defineGetters({ + 'fromImport': (CanonicalizeContext self) => self.fromImport, + 'containingUrl': (CanonicalizeContext self) => + self.containingUrl.andThen(dartToJSUrl), + }); diff --git a/lib/src/js/value/mixin.dart b/lib/src/js/value/mixin.dart index a41b394d2..cc55f3eb4 100644 --- a/lib/src/js/value/mixin.dart +++ b/lib/src/js/value/mixin.dart @@ -13,7 +13,8 @@ import '../utils.dart'; final JSClass mixinClass = () { var jsClass = createJSClass('sass.SassMixin', (Object self) { jsThrow(JsError( - 'It is not possible to construct a SassMixin through the JavaScript API')); + 'It is not possible to construct a SassMixin through the JavaScript ' + 'API')); }); getJSClass(SassMixin(Callable('f', '', (_) => sassNull))) diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 19662c192..fb1f1b614 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -846,7 +846,19 @@ abstract class StylesheetParser extends Parser { FunctionRule _functionRule(LineScannerState start) { var precedingComment = lastSilentComment; lastSilentComment = null; + var beforeName = scanner.state; var name = identifier(normalize: true); + + if (name.startsWith('--')) { + logger.warnForDeprecation( + Deprecation.cssFunctionMixin, + 'Sass @function names beginning with -- are deprecated for forward-' + 'compatibility with plain CSS mixins.\n' + '\n' + 'For details, see https://sass-lang.com/d/css-function-mixin', + span: scanner.spanFrom(beforeName)); + } + whitespace(); var arguments = _argumentDeclaration(); @@ -1261,7 +1273,19 @@ abstract class StylesheetParser extends Parser { MixinRule _mixinRule(LineScannerState start) { var precedingComment = lastSilentComment; lastSilentComment = null; + var beforeName = scanner.state; var name = identifier(normalize: true); + + if (name.startsWith('--')) { + logger.warnForDeprecation( + Deprecation.cssFunctionMixin, + 'Sass @mixin names beginning with -- are deprecated for forward-' + 'compatibility with plain CSS mixins.\n' + '\n' + 'For details, see https://sass-lang.com/d/css-function-mixin', + span: scanner.spanFrom(beforeName)); + } + whitespace(); var arguments = scanner.peekChar() == $lparen ? _argumentDeclaration() diff --git a/lib/src/util/map.dart b/lib/src/util/map.dart index 70037fd64..46982a79c 100644 --- a/lib/src/util/map.dart +++ b/lib/src/util/map.dart @@ -2,6 +2,8 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'option.dart'; + extension MapExtensions on Map { /// If [this] doesn't contain the given [key], sets that key to [value] and /// returns it. @@ -16,4 +18,8 @@ extension MapExtensions on Map { // TODO(nweiz): Remove this once dart-lang/collection#289 is released. /// Like [Map.entries], but returns each entry as a record. Iterable<(K, V)> get pairs => entries.map((e) => (e.key, e.value)); + + /// Returns an option that contains the value at [key] if one exists and null + /// otherwise. + Option getOption(K key) => containsKey(key) ? (this[key]!,) : null; } diff --git a/lib/src/util/option.dart b/lib/src/util/option.dart new file mode 100644 index 000000000..84d296a80 --- /dev/null +++ b/lib/src/util/option.dart @@ -0,0 +1,12 @@ +// Copyright 2024 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. + +/// A type that represents either the presence of a value of type `T` or its +/// absence. +/// +/// When the option is present, this will be a single-element tuple that +/// contains the value. If it's absent, it will be null. This allows callers to +/// distinguish between a present null value and a value that's absent +/// altogether. +typedef Option = (T,)?; diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index ce4105bda..8343a9131 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -1324,6 +1324,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "At-rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var name = await _interpolationToValue(node.name); @@ -1895,6 +1898,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Media rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var queries = await _visitMediaQueries(node.query); @@ -1985,6 +1991,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Style rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "Style rules may not be used within keyframe blocks.", node.span); } var (selectorText, selectorMap) = @@ -2112,6 +2121,9 @@ final class _EvaluateVisitor throw _exception( "Supports rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var condition = CssValue( @@ -3270,6 +3282,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "At-rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } if (node.isChildless) { @@ -3353,6 +3368,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Media rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var mergedQueries = _mediaQueries.andThen( @@ -3401,6 +3419,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Style rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "Style rules may not be used within keyframe blocks.", node.span); } var styleRule = _styleRule; diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 32c4e2764..bbf334ad1 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: 05cb957cd0c7698d8ad648f31d862dc91f0daa7b +// Checksum: 135bf44f65efcbebb4a55b38ada86c754fcdb86b // // ignore_for_file: unused_import @@ -1321,6 +1321,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "At-rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var name = _interpolationToValue(node.name); @@ -1887,6 +1890,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Media rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var queries = _visitMediaQueries(node.query); @@ -1975,6 +1981,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Style rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "Style rules may not be used within keyframe blocks.", node.span); } var (selectorText, selectorMap) = @@ -2102,6 +2111,9 @@ final class _EvaluateVisitor throw _exception( "Supports rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var condition = @@ -3240,6 +3252,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "At-rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } if (node.isChildless) { @@ -3323,6 +3338,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Media rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "At-rules may not be used within keyframe blocks.", node.span); } var mergedQueries = _mediaQueries.andThen( @@ -3369,6 +3387,9 @@ final class _EvaluateVisitor if (_declarationName != null) { throw _exception( "Style rules may not be used within nested declarations.", node.span); + } else if (_inKeyframes && _parent is CssKeyframeBlock) { + throw _exception( + "Style rules may not be used within keyframe blocks.", node.span); } var styleRule = _styleRule; diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 77d3aaa74..d72b35469 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,11 @@ +## 10.3.0 + +* No user-visible changes. + +## 10.2.1 + +* No user-visible changes. + ## 10.2.0 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index ff9a9b383..3d8c3388a 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: 10.2.0 +version: 10.3.0 description: Additional APIs for Dart Sass. homepage: https://github.com/sass/dart-sass @@ -10,7 +10,7 @@ environment: sdk: ">=3.0.0 <4.0.0" dependencies: - sass: 1.75.0 + sass: 1.76.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 54602aa9a..e78af1daa 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.75.0 +version: 1.76.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass