From 783c248d2f673351acea51c447c2e152201170ea Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Mon, 1 Apr 2024 19:42:35 +0200 Subject: [PATCH 1/5] Fix typo in function documentation (#2205) --- lib/src/parse/stylesheet.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 82f3f4565..19662c192 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -552,7 +552,7 @@ abstract class StylesheetParser extends Parser { /// Tries parsing nested children of a declaration whose [name] has already /// been parsed, and returns `null` if it doesn't have any. /// - /// If [value] is passed, it's used as the value of the peroperty without + /// If [value] is passed, it's used as the value of the property without /// nesting. Declaration? _tryDeclarationChildren( Interpolation name, LineScannerState start, From d9220d9c37c638c2bdd847ea1faee401b01742ea Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Wed, 3 Apr 2024 14:15:05 -0700 Subject: [PATCH 2/5] Complete implementation the deprecations API (#2207) --- CHANGELOG.md | 45 +++++++++ bin/sass.dart | 5 +- lib/sass.dart | 12 ++- lib/src/async_compile.dart | 14 ++- lib/src/compile.dart | 16 ++-- lib/src/deprecation.dart | 14 +++ lib/src/embedded/compilation_dispatcher.dart | 22 +++++ lib/src/embedded/logger.dart | 17 ++-- lib/src/executable/options.dart | 31 ++----- lib/src/js.dart | 5 + lib/src/js/compile.dart | 87 +++++++++++++++--- lib/src/js/compile_options.dart | 3 + lib/src/js/deprecations.dart | 65 +++++++++++++ lib/src/js/exports.dart | 2 + lib/src/js/logger.dart | 8 +- lib/src/logger.dart | 43 ++++++++- ...dling.dart => deprecation_processing.dart} | 91 +++++++++++++++---- lib/src/logger/js_to_dart.dart | 13 ++- pkg/sass_api/CHANGELOG.md | 6 +- pkg/sass_api/pubspec.yaml | 4 +- pubspec.yaml | 2 +- test/embedded/utils.dart | 10 +- tool/grind.dart | 2 + 23 files changed, 430 insertions(+), 87 deletions(-) create mode 100644 lib/src/js/deprecations.dart rename lib/src/logger/{deprecation_handling.dart => deprecation_processing.dart} (51%) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5ae69c7c..98eaad010 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,48 @@ +## 1.74.0 + +### JS API + +* Add a new top-level `deprecations` object, which contains various + `Deprecation` objects that define the different types of deprecation used by + the Sass compiler and can be passed to the options below. + +* Add a new `fatalDeprecations` compiler option that causes the compiler to + error if any deprecation warnings of the provided types are encountered. You + can also pass in a `Version` object to treat all deprecations that were active + in that Dart Sass version as fatal. + +* Add a new `futureDeprecations` compiler option that allows you to opt-in to + certain deprecations early (currently just `import`). + +* Add a new `silenceDeprecations` compiler option to ignore any deprecation + warnings of the provided types. + +### Command-Line Interface + +* Add a new `--silence-deprecation` flag, which causes the compiler to ignore + any deprecation warnings of the provided types. + +* Previously, if a future deprecation was passed to `--fatal-deprecation` but + not `--future-deprecation`, it would be treated as fatal despite not being + enabled. Both flags are now required to treat a future deprecation as fatal + with a warning emitted if `--fatal-deprecation` is passed without + `--future-deprecation`, matching the JS API's behavior. + +### Dart API + +* The `compile` methods now take in a `silenceDeprecations` parameter, which + causes the compiler to ignore any deprecation warnings of the provided types. + +* Add `Deprecation.obsoleteIn` to match the JS API. This is currently null for + all deprecations, but will be used once some deprecations become obsolete in + Dart Sass 2.0.0. + +* **Potentially breaking bug fix:** Fix a bug where `compileStringToResultAsync` + ignored `fatalDeprecations` and `futureDeprecations`. + +* The behavior around making future deprecations fatal mentioned in the CLI + section above has also been changed in the Dart API. + ## 1.73.0 * Add support for nesting in plain CSS files. This is not processed by Sass at diff --git a/bin/sass.dart b/bin/sass.dart index d24439eb9..cc912d041 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -15,7 +15,7 @@ import 'package:sass/src/executable/watch.dart'; import 'package:sass/src/import_cache.dart'; import 'package:sass/src/importer/filesystem.dart'; import 'package:sass/src/io.dart'; -import 'package:sass/src/logger/deprecation_handling.dart'; +import 'package:sass/src/logger/deprecation_processing.dart'; import 'package:sass/src/stylesheet_graph.dart'; import 'package:sass/src/utils.dart'; import 'package:sass/src/embedded/executable.dart' @@ -53,7 +53,8 @@ Future main(List args) async { // limit repetition. A separate DeprecationHandlingLogger is created for // each compilation, which will limit repetition if verbose is not // passed in addition to handling fatal/future deprecations. - logger: DeprecationHandlingLogger(options.logger, + logger: DeprecationProcessingLogger(options.logger, + silenceDeprecations: options.silenceDeprecations, fatalDeprecations: options.fatalDeprecations, futureDeprecations: options.futureDeprecations, limitRepetition: false))); diff --git a/lib/sass.dart b/lib/sass.dart index dc94a90fd..eb58adc4f 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -108,6 +108,7 @@ CompileResult compileToResult(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compile(path, @@ -123,6 +124,7 @@ CompileResult compileToResult(String path, verbose: verbose, sourceMap: sourceMap, charset: charset, + silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, futureDeprecations: futureDeprecations); @@ -207,6 +209,7 @@ CompileResult compileStringToResult(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compileString(source, @@ -225,6 +228,7 @@ CompileResult compileStringToResult(String source, verbose: verbose, sourceMap: sourceMap, charset: charset, + silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, futureDeprecations: futureDeprecations); @@ -245,6 +249,7 @@ Future compileToResultAsync(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compileAsync(path, @@ -260,6 +265,7 @@ Future compileToResultAsync(String path, verbose: verbose, sourceMap: sourceMap, charset: charset, + silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, futureDeprecations: futureDeprecations); @@ -285,6 +291,7 @@ Future compileStringToResultAsync(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) => c.compileStringAsync(source, @@ -302,7 +309,10 @@ Future compileStringToResultAsync(String source, quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, - charset: charset); + charset: charset, + silenceDeprecations: silenceDeprecations, + fatalDeprecations: fatalDeprecations, + futureDeprecations: futureDeprecations); /// Like [compileToResult], but returns [CompileResult.css] rather than /// returning [CompileResult] directly. diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index a940d3f26..063f3d2dc 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -17,7 +17,7 @@ import 'importer/legacy_node.dart'; import 'importer/no_op.dart'; import 'io.dart'; import 'logger.dart'; -import 'logger/deprecation_handling.dart'; +import 'logger/deprecation_processing.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/async_evaluate.dart'; @@ -42,10 +42,12 @@ Future compileAsync(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) async { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); @@ -106,10 +108,12 @@ Future compileStringAsync(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) async { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); diff --git a/lib/src/compile.dart b/lib/src/compile.dart index 94221405d..5a7fe54f2 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,7 +5,7 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: a9421a2975e79ad591ae32474cd076e1379d0e75 +// Checksum: ab2c6fa2588988a86abdbe87512134098e01b39e // // ignore_for_file: unused_import @@ -26,7 +26,7 @@ import 'importer/legacy_node.dart'; import 'importer/no_op.dart'; import 'io.dart'; import 'logger.dart'; -import 'logger/deprecation_handling.dart'; +import 'logger/deprecation_processing.dart'; import 'syntax.dart'; import 'utils.dart'; import 'visitor/evaluate.dart'; @@ -51,10 +51,12 @@ CompileResult compile(String path, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); @@ -115,10 +117,12 @@ CompileResult compileString(String source, bool verbose = false, bool sourceMap = false, bool charset = true, + Iterable? silenceDeprecations, Iterable? fatalDeprecations, Iterable? futureDeprecations}) { - DeprecationHandlingLogger deprecationLogger = logger = - DeprecationHandlingLogger(logger ?? Logger.stderr(), + DeprecationProcessingLogger deprecationLogger = logger = + DeprecationProcessingLogger(logger ?? Logger.stderr(), + silenceDeprecations: {...?silenceDeprecations}, fatalDeprecations: {...?fatalDeprecations}, futureDeprecations: {...?futureDeprecations}, limitRepetition: !verbose); diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 3f8540634..a7412e2ce 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -112,14 +112,28 @@ enum Deprecation { /// what version of Dart Sass this deprecation will be live in. final bool isFuture; + /// Underlying version string used by [obsoleteIn]. + /// + /// This is necessary because [Version] doesn't have a constant constructor, + /// so we can't use it directly as an enum property. + final String? _obsoleteIn; + + /// The Dart Sass version this feature was fully removed in, making the + /// deprecation obsolete. + /// + /// For deprecations that are not yet obsolete, this should be null. + Version? get obsoleteIn => _obsoleteIn?.andThen(Version.parse); + /// Constructs a regular deprecation. const Deprecation(this.id, {required String? deprecatedIn, this.description}) : _deprecatedIn = deprecatedIn, + _obsoleteIn = null, isFuture = false; /// Constructs a future deprecation. const Deprecation.future(this.id, {this.description}) : _deprecatedIn = null, + _obsoleteIn = null, isFuture = true; @override diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 356130d84..675216022 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -7,6 +7,7 @@ import 'dart:io'; import 'dart:isolate'; import 'dart:typed_data'; +import 'package:collection/collection.dart'; import 'package:native_synchronization/mailbox.dart'; import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; @@ -124,6 +125,21 @@ final class CompilationDispatcher { : EmbeddedLogger(this, color: request.alertColor, ascii: request.alertAscii); + sass.Deprecation? deprecationOrWarn(String id) { + var deprecation = sass.Deprecation.fromId(id); + if (deprecation == null) { + logger.warn('Invalid deprecation "$id".'); + } + return deprecation; + } + + var fatalDeprecations = + request.fatalDeprecation.map(deprecationOrWarn).whereNotNull(); + var silenceDeprecations = + request.silenceDeprecation.map(deprecationOrWarn).whereNotNull(); + var futureDeprecations = + request.futureDeprecation.map(deprecationOrWarn).whereNotNull(); + try { var importers = request.importers.map((importer) => _decodeImporter(request, importer) ?? @@ -148,6 +164,9 @@ final class CompilationDispatcher { url: input.url.isEmpty ? null : input.url, quietDeps: request.quietDeps, verbose: request.verbose, + fatalDeprecations: fatalDeprecations, + silenceDeprecations: silenceDeprecations, + futureDeprecations: futureDeprecations, sourceMap: request.sourceMap, charset: request.charset); @@ -165,6 +184,9 @@ final class CompilationDispatcher { style: style, quietDeps: request.quietDeps, verbose: request.verbose, + fatalDeprecations: fatalDeprecations, + silenceDeprecations: silenceDeprecations, + futureDeprecations: futureDeprecations, sourceMap: request.sourceMap, charset: request.charset); } on FileSystemException catch (error) { diff --git a/lib/src/embedded/logger.dart b/lib/src/embedded/logger.dart index 331acdf2b..dd1f2a223 100644 --- a/lib/src/embedded/logger.dart +++ b/lib/src/embedded/logger.dart @@ -6,6 +6,7 @@ import 'package:path/path.dart' as p; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; +import '../deprecation.dart'; import '../logger.dart'; import '../util/nullable.dart'; import '../utils.dart'; @@ -14,7 +15,7 @@ import 'embedded_sass.pb.dart' hide SourceSpan; import 'utils.dart'; /// A Sass logger that sends log messages as `LogEvent`s. -final class EmbeddedLogger implements Logger { +final class EmbeddedLogger extends LoggerWithDeprecationType { /// The [CompilationDispatcher] to which to send events. final CompilationDispatcher _dispatcher; @@ -39,16 +40,16 @@ final class EmbeddedLogger implements Logger { ': $message\n'); } - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}) { var formatted = withGlyphs(() { var buffer = StringBuffer(); if (_color) { buffer.write('\u001b[33m\u001b[1m'); - if (deprecation) buffer.write('Deprecation '); + if (deprecation != null) buffer.write('Deprecation '); buffer.write('Warning\u001b[0m'); } else { - if (deprecation) buffer.write('DEPRECATION '); + if (deprecation != null) buffer.write('DEPRECATION '); buffer.write('WARNING'); } if (span == null) { @@ -65,12 +66,14 @@ final class EmbeddedLogger implements Logger { }, ascii: _ascii); var event = OutboundMessage_LogEvent() - ..type = - deprecation ? LogEventType.DEPRECATION_WARNING : LogEventType.WARNING + ..type = deprecation != null + ? LogEventType.DEPRECATION_WARNING + : LogEventType.WARNING ..message = message ..formatted = formatted; if (span != null) event.span = protofySpan(span); if (trace != null) event.stackTrace = trace.toString(); + if (deprecation != null) event.deprecationType = deprecation.id; _dispatcher.sendLog(event); } } diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 876b55814..c0aeaa077 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -95,29 +95,10 @@ final class ExecutableOptions { help: 'Deprecations to treat as errors. You may also pass a Sass\n' 'version to include any behavior deprecated in or before it.\n' 'See https://sass-lang.com/documentation/breaking-changes for \n' - 'a complete list.', - allowedHelp: { - for (var deprecation in Deprecation.values) - if (deprecation - case Deprecation( - deprecatedIn: _?, - :var id, - :var description? - )) - id: description - }) + 'a complete list.') + ..addMultiOption('silence-deprecation', help: 'Deprecations to ignore.') ..addMultiOption('future-deprecation', - help: 'Opt in to a deprecation early.', - allowedHelp: { - for (var deprecation in Deprecation.values) - if (deprecation - case Deprecation( - deprecatedIn: null, - :var id, - :var description? - )) - id: description - }); + help: 'Opt in to a deprecation early.'); parser ..addSeparator(_separator('Other')) @@ -521,6 +502,12 @@ final class ExecutableOptions { : p.absolute(path)); } + /// The set of deprecations whose warnings should be silenced. + Set get silenceDeprecations => { + for (var id in _options['silence-deprecation'] as List) + Deprecation.fromId(id) ?? _fail('Invalid deprecation "$id".') + }; + /// The set of deprecations that cause errors. Set get fatalDeprecations => _fatalDeprecations ??= () { var deprecations = {}; diff --git a/lib/src/js.dart b/lib/src/js.dart index 3246fc743..79bf90180 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -2,7 +2,10 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:js/js_util.dart'; + import 'js/exception.dart'; +import 'js/deprecations.dart'; import 'js/exports.dart'; import 'js/compile.dart'; import 'js/compiler.dart'; @@ -52,6 +55,8 @@ void main() { warn: allowInteropNamed('sass.Logger.silent.warn', (_, __) {}), debug: allowInteropNamed('sass.Logger.silent.debug', (_, __) {}))); exports.NodePackageImporter = nodePackageImporterClass; + exports.deprecations = jsify(deprecations); + exports.Version = versionClass; exports.info = "dart-sass\t${const String.fromEnvironment('version')}\t(Sass Compiler)\t" diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index a08806975..ab5c57cc0 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -7,6 +7,7 @@ import 'package:node_interop/js.dart'; import 'package:node_interop/util.dart' hide futureToPromise; import 'package:term_glyph/term_glyph.dart' as glyph; import 'package:path/path.dart' as p; +import 'package:pub_semver/pub_semver.dart'; import '../../sass.dart'; import '../importer/no_op.dart'; @@ -20,6 +21,7 @@ import '../logger/js_to_dart.dart'; import '../util/nullable.dart'; import 'compile_options.dart'; import 'compile_result.dart'; +import 'deprecations.dart' as js show Deprecation; import 'exception.dart'; import 'importer.dart'; import 'reflection.dart'; @@ -35,6 +37,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { } var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); try { var result = compileToResult(path, color: color, @@ -44,10 +48,15 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers?.map(_parseImporter), - functions: _parseFunctions(options?.functions).cast()); + functions: _parseFunctions(options?.functions).cast(), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); } on SassException catch (error, stackTrace) { @@ -62,6 +71,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { NodeCompileResult compileString(String text, [CompileStringOptions? options]) { var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); try { var result = compileStringToResult(text, syntax: parseSyntax(options?.syntax), @@ -73,12 +84,17 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers?.map(_parseImporter), importer: options?.importer.andThen(_parseImporter) ?? (options?.url == null ? NoOpImporter() : null), - functions: _parseFunctions(options?.functions).cast()); + functions: _parseFunctions(options?.functions).cast(), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); } on SassException catch (error, stackTrace) { @@ -96,6 +112,8 @@ Promise compileAsync(String path, [CompileOptions? options]) { } var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); return _wrapAsyncSassExceptions(futureToPromise(() async { var result = await compileToResultAsync(path, color: color, @@ -105,11 +123,16 @@ Promise compileAsync(String path, [CompileOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers ?.map((importer) => _parseAsyncImporter(importer)), - functions: _parseFunctions(options?.functions, asynch: true)); + functions: _parseFunctions(options?.functions, asynch: true), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); }()), color: color, ascii: ascii); @@ -122,6 +145,8 @@ Promise compileAsync(String path, [CompileOptions? options]) { Promise compileStringAsync(String text, [CompileStringOptions? options]) { var color = options?.alertColor ?? hasTerminal; var ascii = options?.alertAscii ?? glyph.ascii; + var logger = JSToDartLogger(options?.logger, Logger.stderr(color: color), + ascii: ascii); return _wrapAsyncSassExceptions(futureToPromise(() async { var result = await compileStringToResultAsync(text, syntax: parseSyntax(options?.syntax), @@ -133,14 +158,19 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, - logger: JSToDartLogger(options?.logger, Logger.stderr(color: color), - ascii: ascii), + logger: logger, importers: options?.importers ?.map((importer) => _parseAsyncImporter(importer)), importer: options?.importer .andThen((importer) => _parseAsyncImporter(importer)) ?? (options?.url == null ? NoOpImporter() : null), - functions: _parseFunctions(options?.functions, asynch: true)); + functions: _parseFunctions(options?.functions, asynch: true), + fatalDeprecations: _parseDeprecations( + logger, options?.fatalDeprecations, supportVersions: true), + silenceDeprecations: + _parseDeprecations(logger, options?.silenceDeprecations), + futureDeprecations: + _parseDeprecations(logger, options?.futureDeprecations)); return _convertResult(result, includeSourceContents: options?.sourceMapIncludeSources ?? false); }()), color: color, ascii: ascii); @@ -329,6 +359,39 @@ List _parseFunctions(Object? functions, {bool asynch = false}) { return result; } +/// Parses a list of [deprecations] from JS into an list of Dart [Deprecation] +/// objects. +/// +/// [deprecations] can contain deprecation IDs, JS Deprecation objects, and +/// (if [supportVersions] is true) [Version]s. +Iterable? _parseDeprecations( + JSToDartLogger logger, List? deprecations, + {bool supportVersions = false}) { + if (deprecations == null) return null; + return () sync* { + for (var item in deprecations) { + switch (item) { + case String id: + var deprecation = Deprecation.fromId(id); + if (deprecation == null) { + logger.warn('Invalid deprecation "$id".'); + } else { + yield deprecation; + } + case js.Deprecation(:var id): + var deprecation = Deprecation.fromId(id); + if (deprecation == null) { + logger.warn('Invalid deprecation "$id".'); + } else { + yield deprecation; + } + case Version version when supportVersions: + yield* Deprecation.forVersion(version); + } + } + }(); +} + /// The exported `NodePackageImporter` class that can be added to the /// `importers` option to enable loading `pkg:` URLs from `node_modules`. final JSClass nodePackageImporterClass = () { diff --git a/lib/src/js/compile_options.dart b/lib/src/js/compile_options.dart index 1789539d5..c53a1893d 100644 --- a/lib/src/js/compile_options.dart +++ b/lib/src/js/compile_options.dart @@ -23,6 +23,9 @@ class CompileOptions { external JSLogger? get logger; external List? get importers; external Object? get functions; + external List? get fatalDeprecations; + external List? get silenceDeprecations; + external List? get futureDeprecations; } @JS() diff --git a/lib/src/js/deprecations.dart b/lib/src/js/deprecations.dart new file mode 100644 index 000000000..51e3aec1a --- /dev/null +++ b/lib/src/js/deprecations.dart @@ -0,0 +1,65 @@ +// Copyright 2023 Google LLC. 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:js/js.dart'; +import 'package:pub_semver/pub_semver.dart'; + +import '../deprecation.dart' as dart show Deprecation; +import 'reflection.dart'; + +@JS() +@anonymous +class Deprecation { + external String get id; + external String get status; + external String? get description; + external Version? get deprecatedIn; + external Version? get obsoleteIn; + + external factory Deprecation( + {required String id, + required String status, + String? description, + Version? deprecatedIn, + Version? obsoleteIn}); +} + +final Map deprecations = { + for (var deprecation in dart.Deprecation.values) + // `calc-interp` was never actually used, so we don't want to expose it + // in the JS API. + if (deprecation != dart.Deprecation.calcInterp) + deprecation.id: Deprecation( + id: deprecation.id, + status: (() => switch (deprecation) { + dart.Deprecation(isFuture: true) => 'future', + dart.Deprecation(deprecatedIn: null, obsoleteIn: null) => + 'user', + dart.Deprecation(obsoleteIn: null) => 'active', + _ => 'obsolete' + })(), + description: deprecation.description, + deprecatedIn: deprecation.deprecatedIn, + obsoleteIn: deprecation.deprecatedIn), +}; + +/// The JavaScript `Version` class. +final JSClass versionClass = () { + var jsClass = createJSClass('sass.Version', + (Object self, int major, int minor, int patch) { + return Version(major, minor, patch); + }); + + jsClass.defineStaticMethod('parse', (String version) { + var v = Version.parse(version); + if (v.isPreRelease || v.build.isNotEmpty) { + throw FormatException( + 'Build identifiers and prerelease versions not supported.'); + } + return v; + }); + + getJSClass(Version(0, 0, 0)).injectSuperclass(jsClass); + return jsClass; +}(); diff --git a/lib/src/js/exports.dart b/lib/src/js/exports.dart index 9a45268a8..e106ad610 100644 --- a/lib/src/js/exports.dart +++ b/lib/src/js/exports.dart @@ -27,6 +27,8 @@ class Exports { external set Exception(JSClass function); external set Logger(LoggerNamespace namespace); external set NodePackageImporter(JSClass function); + external set deprecations(Object? object); + external set Version(JSClass version); // Value APIs external set Value(JSClass function); diff --git a/lib/src/js/logger.dart b/lib/src/js/logger.dart index fb28030d5..2e6fd6fc0 100644 --- a/lib/src/js/logger.dart +++ b/lib/src/js/logger.dart @@ -5,6 +5,8 @@ import 'package:js/js.dart'; import 'package:source_span/source_span.dart'; +import 'deprecations.dart'; + @JS() @anonymous class JSLogger { @@ -20,11 +22,15 @@ class JSLogger { @anonymous class WarnOptions { external bool get deprecation; + external Deprecation? get deprecationType; external SourceSpan? get span; external String? get stack; external factory WarnOptions( - {required bool deprecation, SourceSpan? span, String? stack}); + {required bool deprecation, + Deprecation? deprecationType, + SourceSpan? span, + String? stack}); } @JS() diff --git a/lib/src/logger.dart b/lib/src/logger.dart index a329b3b79..7569b6e7c 100644 --- a/lib/src/logger.dart +++ b/lib/src/logger.dart @@ -7,7 +7,7 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'deprecation.dart'; -import 'logger/deprecation_handling.dart'; +import 'logger/deprecation_processing.dart'; import 'logger/stderr.dart'; /// An interface for loggers that print messages produced by Sass stylesheets. @@ -37,6 +37,39 @@ abstract class Logger { void debug(String message, SourceSpan span); } +/// A base class for loggers that support the [Deprecation] object, rather than +/// just a boolean flag for whether a warnings is a deprecation warning or not. +/// +/// In Dart Sass 2.0.0, we will eliminate this interface and change +/// [Logger.warn]'s signature to match that of [internalWarn]. This is used +/// in the meantime to provide access to the [Deprecation] object to internal +/// loggers. +/// +/// Implementers should override the protected [internalWarn] method instead of +/// [warn]. +@internal +abstract class LoggerWithDeprecationType implements Logger { + /// This forwards all calls to [internalWarn]. + /// + /// For non-user deprecation warnings, the [warnForDeprecation] extension + /// method should be called instead. + void warn(String message, + {FileSpan? span, Trace? trace, bool deprecation = false}) { + internalWarn(message, + span: span, + trace: trace, + deprecation: deprecation ? Deprecation.userAuthored : null); + } + + /// Equivalent to [Logger.warn], but for internal loggers that support + /// the [Deprecation] object. + /// + /// Subclasses of this logger should override this method instead of [warn]. + @protected + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}); +} + /// An extension to add a `warnForDeprecation` method to loggers without /// making a breaking API change. @internal @@ -44,9 +77,11 @@ extension WarnForDeprecation on Logger { /// Emits a deprecation warning for [deprecation] with the given [message]. void warnForDeprecation(Deprecation deprecation, String message, {FileSpan? span, Trace? trace}) { - if (this case DeprecationHandlingLogger self) { - self.warnForDeprecation(deprecation, message, span: span, trace: trace); - } else if (!deprecation.isFuture) { + if (deprecation.isFuture && this is! DeprecationProcessingLogger) return; + if (this case LoggerWithDeprecationType self) { + self.internalWarn(message, + span: span, trace: trace, deprecation: deprecation); + } else { warn(message, span: span, trace: trace, deprecation: true); } } diff --git a/lib/src/logger/deprecation_handling.dart b/lib/src/logger/deprecation_processing.dart similarity index 51% rename from lib/src/logger/deprecation_handling.dart rename to lib/src/logger/deprecation_processing.dart index 4b185651b..37bd4464f 100644 --- a/lib/src/logger/deprecation_handling.dart +++ b/lib/src/logger/deprecation_processing.dart @@ -3,6 +3,7 @@ // https://opensource.org/licenses/MIT. import 'package:collection/collection.dart'; +import 'package:pub_semver/pub_semver.dart'; import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; @@ -11,17 +12,21 @@ import '../exception.dart'; import '../logger.dart'; /// The maximum number of repetitions of the same warning -/// [DeprecationHandlingLogger] will emit before hiding the rest. +/// [DeprecationProcessingLogger] will emit before hiding the rest. const _maxRepetitions = 5; /// A logger that wraps an inner logger to have special handling for -/// deprecation warnings. -final class DeprecationHandlingLogger implements Logger { +/// deprecation warnings, silencing, making fatal, enabling future, and/or +/// limiting repetition based on its inputs. +final class DeprecationProcessingLogger extends LoggerWithDeprecationType { /// A map of how many times each deprecation has been emitted by this logger. final _warningCounts = {}; final Logger _inner; + /// Deprecation warnings of these types will be ignored. + final Set silenceDeprecations; + /// Deprecation warnings of one of these types will cause an error to be /// thrown. /// @@ -36,27 +41,79 @@ final class DeprecationHandlingLogger implements Logger { /// [_maxRepetitions]. final bool limitRepetition; - DeprecationHandlingLogger(this._inner, - {required this.fatalDeprecations, + DeprecationProcessingLogger(this._inner, + {required this.silenceDeprecations, + required this.fatalDeprecations, required this.futureDeprecations, - this.limitRepetition = true}); + this.limitRepetition = true}) { + for (var deprecation in fatalDeprecations) { + switch (deprecation) { + case Deprecation(isFuture: true) + when !futureDeprecations.contains(deprecation): + warn('Future $deprecation deprecation must be enabled before it can ' + 'be made fatal.'); + case Deprecation(obsoleteIn: Version()): + warn('$deprecation deprecation is obsolete, so does not need to be ' + 'made fatal.'); + case _ when silenceDeprecations.contains(deprecation): + warn('Ignoring setting to silence $deprecation deprecation, since it ' + 'has also been made fatal.'); + default: + // No warning. + } + } - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { - _inner.warn(message, span: span, trace: trace, deprecation: deprecation); + for (var deprecation in silenceDeprecations) { + switch (deprecation) { + case Deprecation.userAuthored: + warn('User-authored deprecations should not be silenced.'); + case Deprecation(obsoleteIn: Version()): + warn('$deprecation deprecation is obsolete. If you were previously ' + 'silencing it, your code may now behave in unexpected ways.'); + case Deprecation(isFuture: true) + when futureDeprecations.contains(deprecation): + warn('Conflicting options for future $deprecation deprecation cancel ' + 'each other out.'); + case Deprecation(isFuture: true): + warn('Future $deprecation deprecation is not yet active, so ' + 'silencing it is unnecessary.'); + default: + // No warning. + } + } + + for (var deprecation in futureDeprecations) { + if (!deprecation.isFuture) { + warn('$deprecation is not a future deprecation, so it does not need to ' + 'be explicitly enabled.'); + } + } + } + + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}) { + if (deprecation != null) { + _handleDeprecation(deprecation, message, span: span, trace: trace); + } else { + _inner.warn(message, span: span, trace: trace); + } } /// Processes a deprecation warning. /// /// If [deprecation] is in [fatalDeprecations], this shows an error. /// - /// If it's a future deprecation that hasn't been opted into or its a + /// If it's a future deprecation that hasn't been opted into or it's a /// deprecation that's already been warned for [_maxReptitions] times and /// [limitRepetitions] is true, the warning is dropped. /// /// Otherwise, this is passed on to [warn]. - void warnForDeprecation(Deprecation deprecation, String message, + void _handleDeprecation(Deprecation deprecation, String message, {FileSpan? span, Trace? trace}) { + if (deprecation.isFuture && !futureDeprecations.contains(deprecation)) { + return; + } + if (fatalDeprecations.contains(deprecation)) { message += "\n\nThis is only an error because you've set the " '$deprecation deprecation to be fatal.\n' @@ -67,10 +124,7 @@ final class DeprecationHandlingLogger implements Logger { _ => SassScriptException(message) }; } - - if (deprecation.isFuture && !futureDeprecations.contains(deprecation)) { - return; - } + if (silenceDeprecations.contains(deprecation)) return; if (limitRepetition) { var count = @@ -78,7 +132,12 @@ final class DeprecationHandlingLogger implements Logger { if (count > _maxRepetitions) return; } - warn(message, span: span, trace: trace, deprecation: true); + if (_inner case LoggerWithDeprecationType inner) { + inner.internalWarn(message, + span: span, trace: trace, deprecation: deprecation); + } else { + _inner.warn(message, span: span, trace: trace, deprecation: true); + } } void debug(String message, SourceSpan span) => _inner.debug(message, span); diff --git a/lib/src/logger/js_to_dart.dart b/lib/src/logger/js_to_dart.dart index aa58a243d..4a11bf546 100644 --- a/lib/src/logger/js_to_dart.dart +++ b/lib/src/logger/js_to_dart.dart @@ -7,11 +7,13 @@ import 'package:source_span/source_span.dart'; import 'package:stack_trace/stack_trace.dart'; import 'package:term_glyph/term_glyph.dart' as glyph; +import '../deprecation.dart'; import '../logger.dart'; +import '../js/deprecations.dart' show deprecations; import '../js/logger.dart'; /// A wrapper around a [JSLogger] that exposes it as a Dart [Logger]. -final class JSToDartLogger implements Logger { +final class JSToDartLogger extends LoggerWithDeprecationType { /// The wrapped logger object. final JSLogger? _node; @@ -27,19 +29,20 @@ final class JSToDartLogger implements Logger { JSToDartLogger(this._node, this._fallback, {bool? ascii}) : _ascii = ascii ?? glyph.ascii; - void warn(String message, - {FileSpan? span, Trace? trace, bool deprecation = false}) { + void internalWarn(String message, + {FileSpan? span, Trace? trace, Deprecation? deprecation}) { if (_node?.warn case var warn?) { warn( message, WarnOptions( span: span ?? (undefined as SourceSpan?), stack: trace.toString(), - deprecation: deprecation)); + deprecation: deprecation != null, + deprecationType: deprecations[deprecation?.id])); } else { _withAscii(() { _fallback.warn(message, - span: span, trace: trace, deprecation: deprecation); + span: span, trace: trace, deprecation: deprecation != null); }); } } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 6e8d05390..32421eb86 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.1.0 + +* No user-visible changes. + ## 10.0.0 * Remove the `allowPlaceholders` argument from `SelectorList.parse()`. Instead, @@ -95,7 +99,7 @@ * All uses of classes from the `tuple` package have been replaced by record types. - + ## 7.2.2 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 4780522de..c6b9f287b 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.0.0 +version: 10.1.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.73.0 + sass: 1.74.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 52c8c58df..9fada08e3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.73.0 +version: 1.74.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass diff --git a/test/embedded/utils.dart b/test/embedded/utils.dart index 68c1e2f83..7741706d6 100644 --- a/test/embedded/utils.dart +++ b/test/embedded/utils.dart @@ -7,6 +7,7 @@ import 'package:test/test.dart'; import 'package:sass/src/embedded/embedded_sass.pb.dart'; import 'package:sass/src/embedded/utils.dart'; +import 'package:sass/src/util/nullable.dart'; import 'embedded_process.dart'; @@ -27,7 +28,10 @@ InboundMessage compileString(String css, bool? sourceMapIncludeSources, Iterable? importers, InboundMessage_CompileRequest_Importer? importer, - Iterable? functions}) { + Iterable? functions, + Iterable? fatalDeprecations, + Iterable? futureDeprecations, + Iterable? silenceDeprecations}) { var input = InboundMessage_CompileRequest_StringInput()..source = css; if (syntax != null) input.syntax = syntax; if (url != null) input.url = url; @@ -43,7 +47,9 @@ InboundMessage compileString(String css, if (functions != null) request.globalFunctions.addAll(functions); if (alertColor != null) request.alertColor = alertColor; if (alertAscii != null) request.alertAscii = alertAscii; - + fatalDeprecations.andThen(request.fatalDeprecation.addAll); + futureDeprecations.andThen(request.futureDeprecation.addAll); + silenceDeprecations.andThen(request.silenceDeprecation.addAll); return InboundMessage()..compileRequest = request; } diff --git a/tool/grind.dart b/tool/grind.dart index 95d9b3cc8..33b56ff2e 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -87,6 +87,8 @@ void main(List args) { 'NULL', 'types', 'NodePackageImporter', + 'deprecations', + 'Version', }; pkg.githubReleaseNotes.fn = () => From 1137797f1731c23e2c7f317a27b40319d57d09b5 Mon Sep 17 00:00:00 2001 From: Jennifer Thakar Date: Wed, 3 Apr 2024 16:41:00 -0700 Subject: [PATCH 3/5] Fix bulma and release 1.74.1 (#2210) --- .github/workflows/test-vendor.yml | 2 +- CHANGELOG.md | 4 ++++ pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-vendor.yml b/.github/workflows/test-vendor.yml index e2d51825e..5e71017a1 100644 --- a/.github/workflows/test-vendor.yml +++ b/.github/workflows/test-vendor.yml @@ -69,4 +69,4 @@ jobs: - run: dart run grinder fetch-bulma env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} - name: Build - run: dart bin/sass.dart --quiet build/bulma/bulma.sass build/bulma-output.css + run: dart bin/sass.dart --quiet build/bulma/bulma.scss build/bulma-output.css diff --git a/CHANGELOG.md b/CHANGELOG.md index 98eaad010..1979177f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.74.1 + +* No user-visible changes. + ## 1.74.0 ### JS API diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 32421eb86..304ec3549 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.1.1 + +* No user-visible changes. + ## 10.1.0 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index c6b9f287b..1adf7da2c 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.1.0 +version: 10.1.1 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.74.0 + sass: 1.74.1 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 9fada08e3..e37160085 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.74.0 +version: 1.74.1 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From c5aff1b2f2ee100a2b93e6644170c557ff4fcd6f Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 10 Apr 2024 17:26:05 -0700 Subject: [PATCH 4/5] Make it possible to build npm with a linked language repo (#2214) This makes several changes: * It renames the `UPDATE_SASS_PROTOCOL` environment variable used by the Grinder `protobuf` task to `UPDATE_SASS_SASS_REPO` to make it more generic and so usable by other tasks. The previous name still works but is considered deprecated. * The `pkg-npm-*` grinder tasks now respects the `UPDATE_SASS_SASS_REPO` environment variable. This allows repos to ensure that the linked language repo's version of the TypeScript types are used when building the npm package. * `UPDATE_SASS_SASS_REPO=false` is set for all the `pkg-npm-*` tasks run by this repo, so that they will use the linked language repo's version of the TypeScript types. Co-authored-by: Carlos (Goodwine) <2022649+Goodwine@users.noreply.github.com> --- .github/util/initialize/action.yml | 2 +- .github/workflows/release.yml | 1 + .github/workflows/test.yml | 4 ++++ tool/grind.dart | 24 ++++++++++++++++++------ 4 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/util/initialize/action.yml b/.github/util/initialize/action.yml index ea8c2f0cf..acfa759c2 100644 --- a/.github/util/initialize/action.yml +++ b/.github/util/initialize/action.yml @@ -32,5 +32,5 @@ runs: - name: Generate Dart from protobuf run: dart run grinder protobuf - env: {UPDATE_SASS_PROTOCOL: false} + env: {UPDATE_SASS_SASS_REPO: false} shell: bash diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7f7c60139..0fe1525f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -70,6 +70,7 @@ jobs: - name: Deploy run: dart run grinder pkg-npm-deploy env: + UPDATE_SASS_SASS_REPO: false NPM_TOKEN: "${{ secrets.NPM_TOKEN }}" deploy_bazel: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index aa31b610e..38bedad96 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -113,6 +113,7 @@ jobs: - name: Build JS run: dart run grinder pkg-npm-dev + env: {UPDATE_SASS_SASS_REPO: false} - name: Check out Sass specification uses: sass/clone-linked-repo@v1 @@ -203,6 +204,7 @@ jobs: - name: Build JS run: dart run grinder pkg-npm-dev + env: {UPDATE_SASS_SASS_REPO: false} - name: Install built dependencies run: npm install @@ -282,6 +284,7 @@ jobs: node-version: ${{ matrix.node-version }} - run: dart run grinder pkg-npm-dev + env: {UPDATE_SASS_SASS_REPO: false} - name: Run tests run: dart run test -t node -j 2 @@ -303,6 +306,7 @@ jobs: github-token: ${{ github.token }} - run: dart run grinder pkg-npm-dev + env: {UPDATE_SASS_SASS_REPO: false} - name: Run tests run: dart run test -p chrome -j 2 env: diff --git a/tool/grind.dart b/tool/grind.dart index 33b56ff2e..631bdebdf 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -213,8 +213,7 @@ String _readAndResolveMarkdown(String path) => File(path) /// Returns a map from JS type declaration file names to their contnets. Map _fetchJSTypes() { - var languageRepo = - cloneOrCheckout("https://github.com/sass/sass", "main", name: 'language'); + var languageRepo = _updateLanguageRepo(); var typeRoot = p.join(languageRepo, 'js-api-doc'); return { @@ -251,10 +250,7 @@ dart run protoc_plugin "\$@" run('chmod', arguments: ['a+x', 'build/protoc-gen-dart']); } - if (Platform.environment['UPDATE_SASS_PROTOCOL'] != 'false') { - cloneOrCheckout("https://github.com/sass/sass.git", "main", - name: 'language'); - } + _updateLanguageRepo(); await runAsync("buf", arguments: ["generate"], @@ -325,3 +321,19 @@ String _updateHomebrewLanguageRevision(String formula) { match.group(0)!.replaceFirst(match.group(1)!, languageRepoRevision) + formula.substring(match.end); } + +/// Clones the main branch of `github.com/sass/sass` and returns the path to the +/// clone. +/// +/// If the `UPDATE_SASS_SASS_REPO` environment variable is `false`, this instead +/// assumes the repo that already exists at `build/language/sass`. +/// `UPDATE_SASS_PROTOCOL` is also checked as a deprecated alias for +/// `UPDATE_SASS_SASS_REPO`. +String _updateLanguageRepo() => + // UPDATE_SASS_PROTOCOL is considered deprecated, because it doesn't apply as + // generically to other tasks. + Platform.environment['UPDATE_SASS_SASS_REPO'] != 'false' && + Platform.environment['UPDATE_SASS_PROTOCOL'] != 'false' + ? cloneOrCheckout("https://github.com/sass/sass.git", "main", + name: 'language') + : 'build/language'; From 821b98e26cc153a2806aff79033ce6b52249ee87 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 11 Apr 2024 15:51:31 -0700 Subject: [PATCH 5/5] Don't cache canonicalize calls when `containingUrl` is available (#2215) See #2208 --- CHANGELOG.md | 10 ++++ lib/src/async_import_cache.dart | 85 ++++++++++++++++++++------------- lib/src/import_cache.dart | 84 ++++++++++++++++++++------------ pkg/sass_api/CHANGELOG.md | 4 ++ pkg/sass_api/pubspec.yaml | 4 +- pubspec.yaml | 2 +- 6 files changed, 124 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1979177f2..ae92aca14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +## 1.75.0 + +* Fix a bug in which stylesheet canonicalization could be cached incorrectly + when custom importers or the Node.js package importer made decisions based on + the URL of the containing stylesheet. + +### JS API + +* Allow `importer` to be passed without `url` in `StringOptionsWithImporter`. + ## 1.74.1 * No user-visible changes. diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 0deb6285f..576094cb4 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -154,64 +154,85 @@ final class AsyncImportCache { } if (baseImporter != null && url.scheme == '') { - var relativeResult = await putIfAbsentAsync( - _relativeCanonicalizeCache, - ( - url, - forImport: forImport, - baseImporter: baseImporter, - baseUrl: baseUrl - ), - () => _canonicalize(baseImporter, baseUrl?.resolveUri(url) ?? url, - baseUrl, forImport)); + 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); + assert( + cacheable, + "Relative loads should always be cacheable because they never " + "provide access to the containing URL."); + return result; + }); if (relativeResult != null) return relativeResult; } - return await putIfAbsentAsync( - _canonicalizeCache, (url, forImport: forImport), () async { - for (var importer in _importers) { - if (await _canonicalize(importer, url, baseUrl, forImport) - case var result?) { + var key = (url, forImport: forImport); + if (_canonicalizeCache.containsKey(key)) return _canonicalizeCache[key]; + + // Each indivudal call to a `canonicalize()` override may not be cacheable + // (specifically, if it has access to `containingUrl` it's too + // context-sensitive to usefully cache). We want to cache a given URL across + // the _entire_ importer chain, so we use [cacheable] to track whether _all_ + // `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) { + 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; } + } - return null; - }); + if (cacheable) _canonicalizeCache[key] = null; + return null; } /// Calls [importer.canonicalize] and prints a deprecation warning if it /// returns a relative URL. /// - /// If [resolveUrl] is `true`, this resolves [url] relative to [baseUrl] - /// before passing it to [importer]. - Future _canonicalize( - AsyncImporter importer, Uri url, Uri? baseUrl, bool forImport, - {bool resolveUrl = false}) async { - var resolved = - resolveUrl && baseUrl != null ? baseUrl.resolveUri(url) : url; + /// This returns both the result of the call to `canonicalize()` and whether + /// 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(resolved)) - : () => importer.canonicalize(resolved); + ? () => 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); - if (result == null) return null; + + // TODO(sass/dart-sass#2208): Determine whether the containing URL was + // _actually_ accessed rather than assuming it was. + var cacheable = !passContainingUrl || importer is FilesystemImporter; + + if (result == null) return (null, cacheable); if (result.scheme == '') { _logger.warnForDeprecation( Deprecation.relativeCanonical, - "Importer $importer canonicalized $resolved to $result.\n" + "Importer $importer canonicalized $url to $result.\n" "Relative canonical URLs are deprecated and will eventually be " "disallowed."); } else if (await importer.isNonCanonicalScheme(result.scheme)) { - throw "Importer $importer canonicalized $resolved to $result, which " - "uses a scheme declared as non-canonical."; + throw "Importer $importer canonicalized $url to $result, which uses a " + "scheme declared as non-canonical."; } - return (importer, result, originalUrl: resolved); + return ((importer, result, originalUrl: url), cacheable); } /// Tries to import [url] using one of this cache's importers. diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index e34f0a7ee..6204971e4 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: d157b83599dbc07a80ac6cb5ffdf5dde03b60376 +// Checksum: 37dd173d676ec6cf201a25b3cca9ac81d92b1433 // // ignore_for_file: unused_import @@ -154,61 +154,85 @@ final class ImportCache { } if (baseImporter != null && url.scheme == '') { - var relativeResult = _relativeCanonicalizeCache.putIfAbsent( - ( - url, - forImport: forImport, - baseImporter: baseImporter, - baseUrl: baseUrl - ), - () => _canonicalize(baseImporter, baseUrl?.resolveUri(url) ?? url, - baseUrl, forImport)); + var relativeResult = _relativeCanonicalizeCache.putIfAbsent(( + url, + forImport: forImport, + baseImporter: baseImporter, + baseUrl: baseUrl + ), () { + var (result, cacheable) = _canonicalize( + baseImporter, baseUrl?.resolveUri(url) ?? url, baseUrl, forImport); + assert( + cacheable, + "Relative loads should always be cacheable because they never " + "provide access to the containing URL."); + return result; + }); if (relativeResult != null) return relativeResult; } - return _canonicalizeCache.putIfAbsent((url, forImport: forImport), () { - for (var importer in _importers) { - if (_canonicalize(importer, url, baseUrl, forImport) case var result?) { + var key = (url, forImport: forImport); + if (_canonicalizeCache.containsKey(key)) return _canonicalizeCache[key]; + + // Each indivudal call to a `canonicalize()` override may not be cacheable + // (specifically, if it has access to `containingUrl` it's too + // context-sensitive to usefully cache). We want to cache a given URL across + // the _entire_ importer chain, so we use [cacheable] to track whether _all_ + // `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) { + 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; } + } - return null; - }); + if (cacheable) _canonicalizeCache[key] = null; + return null; } /// Calls [importer.canonicalize] and prints a deprecation warning if it /// returns a relative URL. /// - /// If [resolveUrl] is `true`, this resolves [url] relative to [baseUrl] - /// before passing it to [importer]. - CanonicalizeResult? _canonicalize( - Importer importer, Uri url, Uri? baseUrl, bool forImport, - {bool resolveUrl = false}) { - var resolved = - resolveUrl && baseUrl != null ? baseUrl.resolveUri(url) : url; + /// This returns both the result of the call to `canonicalize()` and whether + /// that result is cacheable at all. + (CanonicalizeResult?, bool cacheable) _canonicalize( + Importer importer, Uri url, Uri? baseUrl, bool forImport) { var canonicalize = forImport - ? () => inImportRule(() => importer.canonicalize(resolved)) - : () => importer.canonicalize(resolved); + ? () => inImportRule(() => importer.canonicalize(url)) + : () => importer.canonicalize(url); var passContainingUrl = baseUrl != null && (url.scheme == '' || importer.isNonCanonicalScheme(url.scheme)); var result = withContainingUrl(passContainingUrl ? baseUrl : null, canonicalize); - if (result == null) return null; + + // TODO(sass/dart-sass#2208): Determine whether the containing URL was + // _actually_ accessed rather than assuming it was. + var cacheable = !passContainingUrl || importer is FilesystemImporter; + + if (result == null) return (null, cacheable); if (result.scheme == '') { _logger.warnForDeprecation( Deprecation.relativeCanonical, - "Importer $importer canonicalized $resolved to $result.\n" + "Importer $importer canonicalized $url to $result.\n" "Relative canonical URLs are deprecated and will eventually be " "disallowed."); } else if (importer.isNonCanonicalScheme(result.scheme)) { - throw "Importer $importer canonicalized $resolved to $result, which " - "uses a scheme declared as non-canonical."; + throw "Importer $importer canonicalized $url to $result, which uses a " + "scheme declared as non-canonical."; } - return (importer, result, originalUrl: resolved); + return ((importer, result, originalUrl: url), cacheable); } /// Tries to import [url] using one of this cache's importers. diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 304ec3549..77d3aaa74 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 10.2.0 + +* No user-visible changes. + ## 10.1.1 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 1adf7da2c..ff9a9b383 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.1.1 +version: 10.2.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.74.1 + sass: 1.75.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index e37160085..54602aa9a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.74.1 +version: 1.75.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass