From 13830a50c03977117631ac4e5e7366ec4fee0624 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 5 Dec 2023 13:44:28 -0800 Subject: [PATCH 01/35] Fix new recommended lints (#2139) --- lib/src/ast/sass/statement/function_rule.dart | 9 ++------ lib/src/ast/sass/statement/if_rule.dart | 2 +- lib/src/ast/sass/statement/mixin_rule.dart | 9 ++------ lib/src/ast/selector/complex.dart | 5 ++--- lib/src/ast/selector/compound.dart | 6 ++---- lib/src/ast/selector/list.dart | 6 ++---- lib/src/ast/selector/parent.dart | 3 +-- lib/src/ast/selector/simple.dart | 3 +-- lib/src/ast/selector/universal.dart | 3 +-- lib/src/configuration.dart | 3 +-- lib/src/embedded/importer/file.dart | 4 +--- lib/src/embedded/importer/host.dart | 8 +++---- lib/src/exception.dart | 21 +++++++------------ lib/src/extend/functions.dart | 1 + lib/src/parse/at_root_query.dart | 8 ++----- lib/src/parse/css.dart | 4 +--- lib/src/parse/keyframe_selector.dart | 8 ++----- lib/src/parse/media_query.dart | 8 ++----- lib/src/parse/sass.dart | 4 +--- lib/src/parse/scss.dart | 3 +-- lib/src/parse/selector.dart | 14 +++++-------- lib/src/parse/stylesheet.dart | 3 +-- lib/src/value/argument_list.dart | 6 ++---- lib/src/value/number/unitless.dart | 3 +-- 24 files changed, 46 insertions(+), 98 deletions(-) diff --git a/lib/src/ast/sass/statement/function_rule.dart b/lib/src/ast/sass/statement/function_rule.dart index 9242bf858..885bd4ef9 100644 --- a/lib/src/ast/sass/statement/function_rule.dart +++ b/lib/src/ast/sass/statement/function_rule.dart @@ -6,11 +6,8 @@ import 'package:source_span/source_span.dart'; import '../../../util/span.dart'; import '../../../visitor/interface/statement.dart'; -import '../argument_declaration.dart'; import '../declaration.dart'; -import '../statement.dart'; import 'callable_declaration.dart'; -import 'silent_comment.dart'; /// A function declaration. /// @@ -21,10 +18,8 @@ final class FunctionRule extends CallableDeclaration implements SassDeclaration { FileSpan get nameSpan => span.withoutInitialAtRule().initialIdentifier(); - FunctionRule(String name, ArgumentDeclaration arguments, - Iterable children, FileSpan span, - {SilentComment? comment}) - : super(name, arguments, children, span, comment: comment); + FunctionRule(super.name, super.arguments, super.children, super.span, + {super.comment}); T accept(StatementVisitor visitor) => visitor.visitFunctionRule(this); diff --git a/lib/src/ast/sass/statement/if_rule.dart b/lib/src/ast/sass/statement/if_rule.dart index 2a92ac28c..0e611df12 100644 --- a/lib/src/ast/sass/statement/if_rule.dart +++ b/lib/src/ast/sass/statement/if_rule.dart @@ -94,7 +94,7 @@ final class IfClause extends IfRuleClause { /// /// {@category AST} final class ElseClause extends IfRuleClause { - ElseClause(Iterable children) : super(children); + ElseClause(super.children); String toString() => "@else {${children.join(' ')}}"; } diff --git a/lib/src/ast/sass/statement/mixin_rule.dart b/lib/src/ast/sass/statement/mixin_rule.dart index 624eff53e..650e64b65 100644 --- a/lib/src/ast/sass/statement/mixin_rule.dart +++ b/lib/src/ast/sass/statement/mixin_rule.dart @@ -7,12 +7,9 @@ import 'package:source_span/source_span.dart'; import '../../../util/span.dart'; import '../../../visitor/interface/statement.dart'; import '../../../visitor/statement_search.dart'; -import '../argument_declaration.dart'; import '../declaration.dart'; -import '../statement.dart'; import 'callable_declaration.dart'; import 'content_rule.dart'; -import 'silent_comment.dart'; /// A mixin declaration. /// @@ -31,10 +28,8 @@ final class MixinRule extends CallableDeclaration implements SassDeclaration { return startSpan.initialIdentifier(); } - MixinRule(String name, ArgumentDeclaration arguments, - Iterable children, FileSpan span, - {SilentComment? comment}) - : super(name, arguments, children, span, comment: comment); + MixinRule(super.name, super.arguments, super.children, super.span, + {super.comment}); T accept(StatementVisitor visitor) => visitor.visitMixinRule(this); diff --git a/lib/src/ast/selector/complex.dart b/lib/src/ast/selector/complex.dart index 3d97729ca..f6d417901 100644 --- a/lib/src/ast/selector/complex.dart +++ b/lib/src/ast/selector/complex.dart @@ -70,11 +70,10 @@ final class ComplexSelector extends Selector { } ComplexSelector(Iterable> leadingCombinators, - Iterable components, FileSpan span, + Iterable components, super.span, {this.lineBreak = false}) : leadingCombinators = List.unmodifiable(leadingCombinators), - components = List.unmodifiable(components), - super(span) { + components = List.unmodifiable(components) { if (this.leadingCombinators.isEmpty && this.components.isEmpty) { throw ArgumentError( "leadingCombinators and components may not both be empty."); diff --git a/lib/src/ast/selector/compound.dart b/lib/src/ast/selector/compound.dart index c36662cb0..bcc2beb33 100644 --- a/lib/src/ast/selector/compound.dart +++ b/lib/src/ast/selector/compound.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../logger.dart'; @@ -43,9 +42,8 @@ final class CompoundSelector extends Selector { SimpleSelector? get singleSimple => components.length == 1 ? components.first : null; - CompoundSelector(Iterable components, FileSpan span) - : components = List.unmodifiable(components), - super(span) { + CompoundSelector(Iterable components, super.span) + : components = List.unmodifiable(components) { if (this.components.isEmpty) { throw ArgumentError("components may not be empty."); } diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index d432bbfaa..4fc24f7f6 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../exception.dart'; import '../../extend/functions.dart'; @@ -49,9 +48,8 @@ final class SelectorList extends Selector { }), ListSeparator.comma); } - SelectorList(Iterable components, FileSpan span) - : components = List.unmodifiable(components), - super(span) { + SelectorList(Iterable components, super.span) + : components = List.unmodifiable(components) { if (this.components.isEmpty) { throw ArgumentError("components may not be empty."); } diff --git a/lib/src/ast/selector/parent.dart b/lib/src/ast/selector/parent.dart index 18e898652..06f013f05 100644 --- a/lib/src/ast/selector/parent.dart +++ b/lib/src/ast/selector/parent.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../visitor/interface/selector.dart'; import '../selector.dart'; @@ -22,7 +21,7 @@ final class ParentSelector extends SimpleSelector { /// indicating that the parent selector will not be modified. final String? suffix; - ParentSelector(FileSpan span, {this.suffix}) : super(span); + ParentSelector(super.span, {this.suffix}); T accept(SelectorVisitor visitor) => visitor.visitParentSelector(this); diff --git a/lib/src/ast/selector/simple.dart b/lib/src/ast/selector/simple.dart index 0526eed72..d8ae7864d 100644 --- a/lib/src/ast/selector/simple.dart +++ b/lib/src/ast/selector/simple.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../exception.dart'; import '../../logger.dart'; @@ -35,7 +34,7 @@ abstract base class SimpleSelector extends Selector { /// sequence will contain 1000 simple selectors. int get specificity => 1000; - SimpleSelector(FileSpan span) : super(span); + SimpleSelector(super.span); /// Parses a simple selector from [contents]. /// diff --git a/lib/src/ast/selector/universal.dart b/lib/src/ast/selector/universal.dart index d714dcb6a..937377164 100644 --- a/lib/src/ast/selector/universal.dart +++ b/lib/src/ast/selector/universal.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import 'package:meta/meta.dart'; -import 'package:source_span/source_span.dart'; import '../../extend/functions.dart'; import '../../visitor/interface/selector.dart'; @@ -23,7 +22,7 @@ final class UniversalSelector extends SimpleSelector { int get specificity => 0; - UniversalSelector(FileSpan span, {this.namespace}) : super(span); + UniversalSelector(super.span, {this.namespace}); T accept(SelectorVisitor visitor) => visitor.visitUniversalSelector(this); diff --git a/lib/src/configuration.dart b/lib/src/configuration.dart index 1a65d236a..2bb069480 100644 --- a/lib/src/configuration.dart +++ b/lib/src/configuration.dart @@ -119,8 +119,7 @@ final class ExplicitConfiguration extends Configuration { /// Creates a base [ExplicitConfiguration] with a [values] map and a /// [nodeWithSpan]. - ExplicitConfiguration(Map values, this.nodeWithSpan) - : super.implicit(values); + ExplicitConfiguration(super.values, this.nodeWithSpan) : super.implicit(); /// Creates an [ExplicitConfiguration] with a [values] map, a [nodeWithSpan] /// and if this is a copy a reference to the [_originalConfiguration]. diff --git a/lib/src/embedded/importer/file.dart b/lib/src/embedded/importer/file.dart index edd0d3537..d6096eb7b 100644 --- a/lib/src/embedded/importer/file.dart +++ b/lib/src/embedded/importer/file.dart @@ -3,7 +3,6 @@ // https://opensource.org/licenses/MIT. import '../../importer.dart'; -import '../compilation_dispatcher.dart'; import '../embedded_sass.pb.dart' hide SourceSpan; import 'base.dart'; @@ -19,8 +18,7 @@ final class FileImporter extends ImporterBase { /// The host-provided ID of the importer to invoke. final int _importerId; - FileImporter(CompilationDispatcher dispatcher, this._importerId) - : super(dispatcher); + FileImporter(super.dispatcher, this._importerId); Uri? canonicalize(Uri url) { if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); diff --git a/lib/src/embedded/importer/host.dart b/lib/src/embedded/importer/host.dart index 66e60848a..25245721b 100644 --- a/lib/src/embedded/importer/host.dart +++ b/lib/src/embedded/importer/host.dart @@ -6,7 +6,6 @@ import '../../exception.dart'; import '../../importer.dart'; import '../../importer/utils.dart'; import '../../util/span.dart'; -import '../compilation_dispatcher.dart'; import '../embedded_sass.pb.dart' hide SourceSpan; import '../utils.dart'; import 'base.dart'; @@ -20,10 +19,9 @@ final class HostImporter extends ImporterBase { /// [canonicalize]. final Set _nonCanonicalSchemes; - HostImporter(CompilationDispatcher dispatcher, this._importerId, - Iterable nonCanonicalSchemes) - : _nonCanonicalSchemes = Set.unmodifiable(nonCanonicalSchemes), - super(dispatcher) { + HostImporter( + super.dispatcher, this._importerId, Iterable nonCanonicalSchemes) + : _nonCanonicalSchemes = Set.unmodifiable(nonCanonicalSchemes) { for (var scheme in _nonCanonicalSchemes) { if (isValidUrlScheme(scheme)) continue; throw SassException( diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 38a1a057e..69dbbe7e5 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -28,10 +28,9 @@ class SassException extends SourceSpanException { /// compilation, before it failed. final Set loadedUrls; - SassException(String message, FileSpan span, [Iterable? loadedUrls]) + SassException(super.message, FileSpan super.span, [Iterable? loadedUrls]) : loadedUrls = - loadedUrls == null ? const {} : Set.unmodifiable(loadedUrls), - super(message, span); + loadedUrls == null ? const {} : Set.unmodifiable(loadedUrls); /// Converts this to a [MultiSpanSassException] with the additional [span] and /// [label]. @@ -224,9 +223,7 @@ class SassFormatException extends SassException SassFormatException withLoadedUrls(Iterable loadedUrls) => SassFormatException(message, span, loadedUrls); - SassFormatException(String message, FileSpan span, - [Iterable? loadedUrls]) - : super(message, span, loadedUrls); + SassFormatException(super.message, super.span, [super.loadedUrls]); } /// A [SassFormatException] that's also a [MultiSpanFormatException]. @@ -248,10 +245,9 @@ class MultiSpanSassFormatException extends MultiSpanSassException MultiSpanSassFormatException( message, span, primaryLabel, secondarySpans, loadedUrls); - MultiSpanSassFormatException(String message, FileSpan span, - String primaryLabel, Map secondarySpans, - [Iterable? loadedUrls]) - : super(message, span, primaryLabel, secondarySpans, loadedUrls); + MultiSpanSassFormatException( + super.message, super.span, super.primaryLabel, super.secondarySpans, + [super.loadedUrls]); } /// An exception thrown by SassScript. @@ -287,9 +283,8 @@ class MultiSpanSassScriptException extends SassScriptException { final Map secondarySpans; MultiSpanSassScriptException( - String message, this.primaryLabel, Map secondarySpans) - : secondarySpans = Map.unmodifiable(secondarySpans), - super(message); + super.message, this.primaryLabel, Map secondarySpans) + : secondarySpans = Map.unmodifiable(secondarySpans); /// Converts this to a [SassException] with the given primary [span]. MultiSpanSassException withSpan(FileSpan span) => diff --git a/lib/src/extend/functions.dart b/lib/src/extend/functions.dart index 6299c5fcf..01d70d248 100644 --- a/lib/src/extend/functions.dart +++ b/lib/src/extend/functions.dart @@ -9,6 +9,7 @@ /// aren't instance methods on other objects because their APIs aren't a good /// fit—usually because they deal with raw component lists rather than selector /// classes, to reduce allocations. +library; import 'dart:collection'; diff --git a/lib/src/parse/at_root_query.dart b/lib/src/parse/at_root_query.dart index 11eee11f2..a107458c1 100644 --- a/lib/src/parse/at_root_query.dart +++ b/lib/src/parse/at_root_query.dart @@ -5,16 +5,12 @@ import 'package:charcode/charcode.dart'; import '../ast/sass.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import 'parser.dart'; /// A parser for `@at-root` queries. class AtRootQueryParser extends Parser { - AtRootQueryParser(String contents, - {Object? url, Logger? logger, InterpolationMap? interpolationMap}) - : super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + AtRootQueryParser(super.contents, + {super.url, super.logger, super.interpolationMap}); AtRootQuery parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index 6ed9123b7..754a3614a 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -7,7 +7,6 @@ import 'package:string_scanner/string_scanner.dart'; import '../ast/sass.dart'; import '../functions.dart'; -import '../logger.dart'; import 'scss.dart'; /// The set of all function names disallowed in plain CSS. @@ -31,8 +30,7 @@ final _disallowedFunctionNames = class CssParser extends ScssParser { bool get plainCss => true; - CssParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + CssParser(super.contents, {super.url, super.logger}); void silentComment() { var start = scanner.state; diff --git a/lib/src/parse/keyframe_selector.dart b/lib/src/parse/keyframe_selector.dart index 71908c3e3..25a690813 100644 --- a/lib/src/parse/keyframe_selector.dart +++ b/lib/src/parse/keyframe_selector.dart @@ -4,17 +4,13 @@ import 'package:charcode/charcode.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import '../util/character.dart'; import 'parser.dart'; /// A parser for `@keyframes` block selectors. class KeyframeSelectorParser extends Parser { - KeyframeSelectorParser(String contents, - {Object? url, Logger? logger, InterpolationMap? interpolationMap}) - : super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + KeyframeSelectorParser(super.contents, + {super.url, super.logger, super.interpolationMap}); List parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/media_query.dart b/lib/src/parse/media_query.dart index be86a1994..ce54dae57 100644 --- a/lib/src/parse/media_query.dart +++ b/lib/src/parse/media_query.dart @@ -5,17 +5,13 @@ import 'package:charcode/charcode.dart'; import '../ast/css.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import '../utils.dart'; import 'parser.dart'; /// A parser for `@media` queries. class MediaQueryParser extends Parser { - MediaQueryParser(String contents, - {Object? url, Logger? logger, InterpolationMap? interpolationMap}) - : super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + MediaQueryParser(super.contents, + {super.url, super.logger, super.interpolationMap}); List parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/sass.dart b/lib/src/parse/sass.dart index 95fd054c5..78dea3179 100644 --- a/lib/src/parse/sass.dart +++ b/lib/src/parse/sass.dart @@ -7,7 +7,6 @@ import 'package:string_scanner/string_scanner.dart'; import '../ast/sass.dart'; import '../interpolation_buffer.dart'; -import '../logger.dart'; import '../util/character.dart'; import '../value.dart'; import 'stylesheet.dart'; @@ -38,8 +37,7 @@ class SassParser extends StylesheetParser { bool get indented => true; - SassParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + SassParser(super.contents, {super.url, super.logger}); Interpolation styleRuleSelector() { var start = scanner.state; diff --git a/lib/src/parse/scss.dart b/lib/src/parse/scss.dart index cc432e9c8..67b5c0f4b 100644 --- a/lib/src/parse/scss.dart +++ b/lib/src/parse/scss.dart @@ -16,8 +16,7 @@ class ScssParser extends StylesheetParser { bool get indented => false; int get currentIndentation => 0; - ScssParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + ScssParser(super.contents, {super.url, super.logger}); Interpolation styleRuleSelector() => almostAnyValue(); diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 75df8b205..0e5b7a04f 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -6,8 +6,6 @@ import 'package:charcode/charcode.dart'; import '../ast/css/value.dart'; import '../ast/selector.dart'; -import '../interpolation_map.dart'; -import '../logger.dart'; import '../util/character.dart'; import '../utils.dart'; import 'parser.dart'; @@ -36,16 +34,14 @@ class SelectorParser extends Parser { /// Whether this parser allows placeholder selectors beginning with `%`. final bool _allowPlaceholder; - SelectorParser(String contents, - {Object? url, - Logger? logger, - InterpolationMap? interpolationMap, + SelectorParser(super.contents, + {super.url, + super.logger, + super.interpolationMap, bool allowParent = true, bool allowPlaceholder = true}) : _allowParent = allowParent, - _allowPlaceholder = allowPlaceholder, - super(contents, - url: url, logger: logger, interpolationMap: interpolationMap); + _allowPlaceholder = allowPlaceholder; SelectorList parse() { return wrapSpanFormatException(() { diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 23c53952e..15c568f66 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -69,8 +69,7 @@ abstract class StylesheetParser extends Parser { @protected SilentComment? lastSilentComment; - StylesheetParser(String contents, {Object? url, Logger? logger}) - : super(contents, url: url, logger: logger); + StylesheetParser(super.contents, {super.url, super.logger}); // ## Statements diff --git a/lib/src/value/argument_list.dart b/lib/src/value/argument_list.dart index f9b4b5014..23de2db7a 100644 --- a/lib/src/value/argument_list.dart +++ b/lib/src/value/argument_list.dart @@ -42,8 +42,6 @@ class SassArgumentList extends SassList { bool get wereKeywordsAccessed => _wereKeywordsAccessed; var _wereKeywordsAccessed = false; - SassArgumentList(Iterable contents, Map keywords, - ListSeparator separator) - : _keywords = Map.unmodifiable(keywords), - super(contents, separator); + SassArgumentList(super.contents, Map keywords, super.separator) + : _keywords = Map.unmodifiable(keywords); } diff --git a/lib/src/value/number/unitless.dart b/lib/src/value/number/unitless.dart index 7272b7c59..a81d1d9a9 100644 --- a/lib/src/value/number/unitless.dart +++ b/lib/src/value/number/unitless.dart @@ -19,8 +19,7 @@ class UnitlessSassNumber extends SassNumber { bool get hasUnits => false; bool get hasComplexUnits => false; - UnitlessSassNumber(double value, [(SassNumber, SassNumber)? asSlash]) - : super.protected(value, asSlash); + UnitlessSassNumber(super.value, [super.asSlash]) : super.protected(); SassNumber withValue(num value) => UnitlessSassNumber(value.toDouble()); From bd80c58752f03a758683cb8eb3efefccf94ae1b0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 5 Dec 2023 15:47:29 -0800 Subject: [PATCH 02/35] Make LazyFileSpans work in JavaScript (#2142) Closes #1952 --- CHANGELOG.md | 7 +++++++ lib/src/js/source_span.dart | 4 +++- pubspec.yaml | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ff3e06fb..77f7bf3ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.69.6 + +### JS API + +* Fix a bug where certain exceptions could produce `SourceSpan`s that didn't + follow the documented `SourceSpan` API. + ## 1.69.5 ### JS API diff --git a/lib/src/js/source_span.dart b/lib/src/js/source_span.dart index 998db03ad..057aec500 100644 --- a/lib/src/js/source_span.dart +++ b/lib/src/js/source_span.dart @@ -4,6 +4,7 @@ import 'package:source_span/source_span.dart'; +import '../util/lazy_file_span.dart'; import '../util/multi_span.dart'; import '../util/nullable.dart'; import 'reflection.dart'; @@ -14,8 +15,9 @@ import 'utils.dart'; void updateSourceSpanPrototype() { var span = SourceFile.fromString('').span(0); var multiSpan = MultiSpan(span, '', {}); + var lazySpan = LazyFileSpan(() => span); - for (var item in [span, multiSpan]) { + for (var item in [span, multiSpan, lazySpan]) { getJSClass(item).defineGetters({ 'start': (FileSpan span) => span.start, 'end': (FileSpan span) => span.end, diff --git a/pubspec.yaml b/pubspec.yaml index 9e28af5df..6f666281e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.69.5 +version: 1.69.6-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From cd798bfb7b812233e18b7f67ffd8aada1e9b44e0 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 7 Dec 2023 16:26:18 -0800 Subject: [PATCH 03/35] Improve inspect() output for complex units (#2138) Closes #2070 --- CHANGELOG.md | 3 ++ lib/src/visitor/serialize.dart | 62 ++++++++++++++++++++------------ test/embedded/function_test.dart | 8 ++--- test/output_test.dart | 51 ++++++++++++++++++++++++-- 4 files changed, 95 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f7bf3ea..67c678c5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.69.6 +* Produce better output for numbers with complex units in `meta.inspect()` and + debugging messages. + ### JS API * Fix a bug where certain exceptions could produce `SourceSpan`s that didn't diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 7073c4b4f..676a4876b 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -6,7 +6,6 @@ import 'dart:math' as math; import 'dart:typed_data'; import 'package:charcode/charcode.dart'; -import 'package:collection/collection.dart'; import 'package:source_maps/source_maps.dart'; import 'package:string_scanner/string_scanner.dart'; @@ -489,13 +488,8 @@ final class _SerializeVisitor void _writeCalculationValue(Object value) { switch (value) { - case SassNumber(value: double(isFinite: false), hasComplexUnits: true): - if (!_inspect) { - throw SassScriptException("$value isn't a valid CSS value."); - } - - _writeNumber(value.value); - _buffer.write(value.unitString); + case SassNumber(hasComplexUnits: true) when !_inspect: + throw SassScriptException("$value isn't a valid CSS value."); case SassNumber(value: double(isFinite: false)): switch (value.value) { @@ -507,12 +501,15 @@ final class _SerializeVisitor _buffer.write('NaN'); } - if (value.numeratorUnits.firstOrNull case var unit?) { - _writeOptionalSpace(); - _buffer.writeCharCode($asterisk); - _writeOptionalSpace(); - _buffer.writeCharCode($1); - _buffer.write(unit); + _writeCalculationUnits(value.numeratorUnits, value.denominatorUnits); + + case SassNumber(hasComplexUnits: true): + _writeNumber(value.value); + if (value.numeratorUnits case [var first, ...var rest]) { + _buffer.write(first); + _writeCalculationUnits(rest, value.denominatorUnits); + } else { + _writeCalculationUnits([], value.denominatorUnits); } case Value(): @@ -534,14 +531,36 @@ final class _SerializeVisitor _parenthesizeCalculationRhs(operator, right.operator)) || (operator == CalculationOperator.dividedBy && right is SassNumber && - !right.value.isFinite && - right.hasUnits); + (right.value.isFinite + ? right.hasComplexUnits + : right.hasUnits)); if (parenthesizeRight) _buffer.writeCharCode($lparen); _writeCalculationValue(right); if (parenthesizeRight) _buffer.writeCharCode($rparen); } } + /// Writes the complex numerator and denominator units beyond the first + /// numerator unit for a number as they appear in a calculation. + void _writeCalculationUnits( + List numeratorUnits, List denominatorUnits) { + for (var unit in numeratorUnits) { + _writeOptionalSpace(); + _buffer.writeCharCode($asterisk); + _writeOptionalSpace(); + _buffer.writeCharCode($1); + _buffer.write(unit); + } + + for (var unit in denominatorUnits) { + _writeOptionalSpace(); + _buffer.writeCharCode($slash); + _writeOptionalSpace(); + _buffer.writeCharCode($1); + _buffer.write(unit); + } + } + /// Returns whether the right-hand operation of a calculation should be /// parenthesized. /// @@ -787,16 +806,15 @@ final class _SerializeVisitor return; } - _writeNumber(value.value); - - if (!_inspect) { - if (value.hasComplexUnits) { + if (value.hasComplexUnits) { + if (!_inspect) { throw SassScriptException("$value isn't a valid CSS value."); } - if (value.numeratorUnits case [var first]) _buffer.write(first); + visitCalculation(SassCalculation.unsimplified('calc', [value])); } else { - _buffer.write(value.unitString); + _writeNumber(value.value); + if (value.numeratorUnits case [var first]) _buffer.write(first); } } diff --git a/test/embedded/function_test.dart b/test/embedded/function_test.dart index d927a780f..97e79f334 100644 --- a/test/embedded/function_test.dart +++ b/test/embedded/function_test.dart @@ -912,7 +912,7 @@ void main() { ..value = 1 ..numerators.addAll(["em", "px", "foo"])), inspect: true), - "1em*px*foo"); + "calc(1em * 1px * 1foo)"); }); test("with one denominator", () async { @@ -923,7 +923,7 @@ void main() { ..value = 1 ..denominators.add("em")), inspect: true), - "1em^-1"); + "calc(1 / 1em)"); }); test("with multiple denominators", () async { @@ -934,7 +934,7 @@ void main() { ..value = 1 ..denominators.addAll(["em", "px", "foo"])), inspect: true), - "1(em*px*foo)^-1"); + "calc(1 / 1em / 1px / 1foo)"); }); test("with numerators and denominators", () async { @@ -946,7 +946,7 @@ void main() { ..numerators.addAll(["em", "px"]) ..denominators.addAll(["s", "foo"])), inspect: true), - "1em*px/s*foo"); + "calc(1em * 1px / 1s / 1foo)"); }); }); diff --git a/test/output_test.dart b/test/output_test.dart index 2023ae65b..c9dcfca1a 100644 --- a/test/output_test.dart +++ b/test/output_test.dart @@ -113,10 +113,55 @@ void main() { }); }); - // Tests for sass/dart-sass#417. + // Tests for sass/dart-sass#2070. // - // Note there's no need for "in Sass" cases as it's not possible to have - // trailing loud comments in the Sass syntax. + // These aren't covered by sass-spec because the inspect format for + // non-literal values isn't covered by the spec. + group("uses a nice format to inspect numbers with complex units", () { + group("finite", () { + test("top-level", () { + expect(compileString(""" + @use 'sass:meta'; + a {b: meta.inspect(1px * 1em)}; + """), equalsIgnoringWhitespace('a { b: calc(1px * 1em); }')); + }); + + test("in calc", () { + expect(compileString(""" + @use 'sass:meta'; + a {b: meta.inspect(calc(1px * 1em))}; + """), equalsIgnoringWhitespace('a { b: calc(1px * 1em); }')); + }); + + test("nested in calc", () { + expect(compileString(""" + @use 'sass:meta'; + a {b: meta.inspect(calc(c / (1px * 1em)))}; + """), equalsIgnoringWhitespace('a { b: calc(c / (1px * 1em)); }')); + }); + + test("numerator and denominator", () { + expect(compileString(""" + @use 'sass:math'; + @use 'sass:meta'; + a {b: meta.inspect(1px * math.div(math.div(1em, 1s), 1x))}; + """), equalsIgnoringWhitespace('a { b: calc(1px * 1em / 1s / 1x); }')); + }); + + test("denominator only", () { + expect(compileString(""" + @use 'sass:math'; + @use 'sass:meta'; + a {b: meta.inspect(math.div(math.div(1, 1s), 1x))}; + """), equalsIgnoringWhitespace('a { b: calc(1 / 1s / 1x); }')); + }); + }); + }); + + // Tests for sass/dart-sass#417. + // + // Note there's no need for "in Sass" cases as it's not possible to have + // trailing loud comments in the Sass syntax. group("preserve trailing loud comments in SCSS", () { test("after open block", () { expect(compileString(""" From 4daf0b4ec12094624626c9ac4489c67f2369dc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Thu, 7 Dec 2023 16:38:33 -0800 Subject: [PATCH 04/35] Escape non-US-ASCII characters in `SassException.toCssString()` (#2143) Co-authored-by: Natalie Weizenbaum --- CHANGELOG.md | 4 ++++ lib/src/exception.dart | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 67c678c5e..31b33c809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ * Produce better output for numbers with complex units in `meta.inspect()` and debugging messages. +* When generating CSS error messages to display in-browser, escape all code + points that aren't in the US-ASCII region. Previously only code points U+0100 + LATIN CAPITAL LETTER A WITH MACRON were escaped. + ### JS API * Fix a bug where certain exceptions could produce `SourceSpan`s that didn't diff --git a/lib/src/exception.dart b/lib/src/exception.dart index 69dbbe7e5..898258c88 100644 --- a/lib/src/exception.dart +++ b/lib/src/exception.dart @@ -82,12 +82,12 @@ class SassException extends SourceSpanException { .replaceAll("\r\n", "\n"); term_glyph.ascii = wasAscii; - // For the string comment, render all non-ASCII characters as escape + // For the string comment, render all non-US-ASCII characters as escape // sequences so that they'll show up even if the HTTP headers are set // incorrectly. var stringMessage = StringBuffer(); for (var rune in SassString(toString(color: false)).toString().runes) { - if (rune > 0xFF) { + if (rune > 0x7F) { stringMessage ..writeCharCode($backslash) ..write(rune.toRadixString(16)) From f5dab76b4ec6240bccb049cec412ef26f9d68a0c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Dec 2023 13:45:13 -0800 Subject: [PATCH 05/35] Bump dartdoc from 7.0.2 to 8.0.2 (#2146) Bumps [dartdoc](https://github.com/dart-lang/dartdoc) from 7.0.2 to 8.0.2. - [Release notes](https://github.com/dart-lang/dartdoc/releases) - [Changelog](https://github.com/dart-lang/dartdoc/blob/main/CHANGELOG.md) - [Commits](https://github.com/dart-lang/dartdoc/compare/v7.0.2...v8.0.2) --- updated-dependencies: - dependency-name: dartdoc dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 6f666281e..c42d2133e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -42,7 +42,7 @@ dev_dependencies: archive: ^3.1.2 crypto: ^3.0.0 dart_style: ^2.0.0 - dartdoc: ">=6.0.0 <8.0.0" + dartdoc: ">=6.0.0 <9.0.0" grinder: ^0.9.0 node_preamble: ^2.0.2 lints: ">=2.0.0 <4.0.0" From 6f665c1dd2b3a9d005a62dbaab65df0a3a381111 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Mon, 11 Dec 2023 13:47:33 -0800 Subject: [PATCH 06/35] Escape unprintable 0x7F (delete control character) (#2144) Co-authored-by: Natalie Weizenbaum --- CHANGELOG.md | 2 ++ lib/src/visitor/serialize.dart | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31b33c809..a4995a0cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * Produce better output for numbers with complex units in `meta.inspect()` and debugging messages. +* Escape U+007F DELETE when serializing strings. + * When generating CSS error messages to display in-browser, escape all code points that aren't in the US-ASCII region. Previously only code points U+0100 LATIN CAPITAL LETTER A WITH MACRON were escaped. diff --git a/lib/src/visitor/serialize.dart b/lib/src/visitor/serialize.dart index 676a4876b..9ab203073 100644 --- a/lib/src/visitor/serialize.dart +++ b/lib/src/visitor/serialize.dart @@ -1090,7 +1090,8 @@ final class _SerializeVisitor $fs || $gs || $rs || - $us: + $us || + $del: _writeEscape(buffer, char, string, i); case $backslash: From 1fc740db40be58549c7e6bbab6820fd45d0ff1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Fri, 15 Dec 2023 14:34:26 -0800 Subject: [PATCH 07/35] Upload releases for musl-libc and android (#2149) Co-authored-by: Natalie Weizenbaum --- .github/util/initialize/action.yml | 2 +- .github/workflows/build-android.yml | 71 +++ .github/workflows/build-linux-musl.yml | 89 ++++ .github/workflows/build-linux.yml | 62 +++ .github/workflows/build-macos.yml | 38 ++ .github/workflows/build-windows.yml | 41 ++ .github/workflows/ci.yml | 654 +------------------------ .github/workflows/release.yml | 219 +++++++++ .github/workflows/test-vendor.yml | 72 +++ .github/workflows/test.yml | 309 ++++++++++++ CHANGELOG.md | 2 + 11 files changed, 921 insertions(+), 638 deletions(-) create mode 100644 .github/workflows/build-android.yml create mode 100644 .github/workflows/build-linux-musl.yml create mode 100644 .github/workflows/build-linux.yml create mode 100644 .github/workflows/build-macos.yml create mode 100644 .github/workflows/build-windows.yml create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test-vendor.yml create mode 100644 .github/workflows/test.yml diff --git a/.github/util/initialize/action.yml b/.github/util/initialize/action.yml index ad2abda5e..30252d356 100644 --- a/.github/util/initialize/action.yml +++ b/.github/util/initialize/action.yml @@ -2,7 +2,7 @@ name: Initialize description: Check out Dart Sass and build the embedded protocol buffer. inputs: github-token: {required: true} - node-version: {required: false, default: 18} + node-version: {required: false, default: 'lts/*'} dart-sdk: {required: false, default: stable} architecture: {required: false} runs: diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml new file mode 100644 index 000000000..5f2bb57db --- /dev/null +++ b/.github/workflows/build-android.yml @@ -0,0 +1,71 @@ +name: Build for android + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + lib: lib64 + platform: linux/amd64 + - arch: ia32 + lib: lib + platform: linux/amd64 + - arch: arm64 + lib: lib64 + platform: linux/arm64 + - arch: arm + lib: lib + platform: linux/arm64 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + with: + image: tonistiigi/binfmt:master # need qemu >= 7.0.0 + + - name: Compile Protobuf + run: | + docker run --rm -i \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart <<'EOF' + set -e + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m).tar.gz" | tar -xzC /usr/local --strip-components 1 + dart pub get + dart run grinder protobuf + EOF + + - name: Build + run: | + docker run --rm -i \ + --platform ${{ matrix.platform }} \ + --privileged \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + ghcr.io/dart-android/dart <<'EOF' + set -e + export DART_SDK=/system/${{ matrix.lib }}/dart + export PATH=$DART_SDK/bin:$PATH + dart pub get + dart run grinder pkg-standalone-android-${{ matrix.arch }} + EOF + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-android-${{ matrix.arch }} + path: build/*.tar.gz + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/build-linux-musl.yml b/.github/workflows/build-linux-musl.yml new file mode 100644 index 000000000..050292f4a --- /dev/null +++ b/.github/workflows/build-linux-musl.yml @@ -0,0 +1,89 @@ +name: Build for linux-musl + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + platform: linux/amd64 + - arch: ia32 + platform: linux/386 + - arch: arm64 + platform: linux/arm64 + - arch: arm + platform: linux/arm/v7 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Compile Protobuf + run: | + docker run --rm -i \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart <<'EOF' + set -e + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m).tar.gz" | tar -xzC /usr/local --strip-components 1 + dart pub get + dart run grinder protobuf + EOF + + # https://gitlab.com/qemu-project/qemu/-/issues/1729 + # + # There is a bug in qemu's mremap causing pthread_getattr_np in musl to stuck in a loop on arm. + # Unless qemu fixes the bug or we get a real linux-arm runner, we cannot build aot-snapshot + # for arm on CI. So, we create a kernel snapshot for arm build in amd64 container instead. + - name: Build + run: | + docker run --rm -i \ + --platform ${{ matrix.arch == 'arm' && 'linux/amd64' || matrix.platform }} \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + ghcr.io/dart-musl/dart <<'EOF' + set -e + dart pub get + dart run grinder pkg-standalone-linux-${{ matrix.arch }} + # Rename the artifact from -linux- to -linux-musl- to avoid conflict with glibc builds. + find build -name '*.tar.gz' -print0 | xargs -0 -n 1 -- sh -xc 'mv "$1" "$(echo "$1" | sed -e "s/linux/linux-musl/")"' -- + EOF + + # The kernel snapshot created for arm in the previous step is bundling a glibc based dart runtime + # due to how cli_pkg downloads the sdk for building non-native platforms. Therefore we need to + # replace it with musl-libc based dart runtime to create a working linux-musl-arm package. + - name: Fix Dart Runtime + if: matrix.arch == 'arm' + run: | + docker run --rm -i \ + --platform ${{ matrix.platform }} \ + --volume $PWD:$PWD \ + --workdir $PWD \ + ghcr.io/dart-musl/dart <<'EOF' + set -e + apk add --no-cache tar + cd build + DART_RUNTIME=$(tar -tzf *.tar.gz --wildcards "*/src/dart") + tar -xzf *.tar.gz + cp $DART_SDK/bin/dart $DART_RUNTIME + tar -czf *.tar.gz "$(dirname "$(dirname "$DART_RUNTIME")")" + EOF + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-linux-musl-${{ matrix.arch }} + path: build/*.tar.gz + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml new file mode 100644 index 000000000..e36628c4a --- /dev/null +++ b/.github/workflows/build-linux.yml @@ -0,0 +1,62 @@ +name: Build for linux + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + platform: linux/amd64 + - arch: ia32 + platform: linux/amd64 + - arch: arm + platform: linux/arm/v7 + - arch: arm64 + platform: linux/arm64 + + steps: + - uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Compile Protobuf + run: | + docker run --rm -i \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart <<'EOF' + set -e + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" "https://github.com/bufbuild/buf/releases/latest/download/buf-$(uname -s)-$(uname -m).tar.gz" | tar -xzC /usr/local --strip-components 1 + dart pub get + dart run grinder protobuf + EOF + + - name: Build + run: | + docker run --rm -i \ + --platform ${{ matrix.platform }} \ + --volume "$PWD:$PWD" \ + --workdir "$PWD" \ + docker.io/library/dart:latest <<'EOF' + set -e + dart pub get + dart run grinder pkg-standalone-linux-${{ matrix.arch }} + EOF + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-linux-${{ matrix.arch }} + path: build/*.tar.gz + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 000000000..7d31257bc --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,38 @@ +name: Build for macos + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ${{ matrix.runner }} + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runner: macos-latest + # https://github.blog/2023-10-02-introducing-the-new-apple-silicon-powered-m1-macos-larger-runner-for-github-actions/ + - arch: arm64 + runner: macos-latest-xlarge + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Build + run: dart run grinder pkg-standalone-macos-${{ matrix.arch }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-macos-${{ matrix.arch }} + path: build/*.tar.gz + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml new file mode 100644 index 000000000..7036dfa34 --- /dev/null +++ b/.github/workflows/build-windows.yml @@ -0,0 +1,41 @@ +name: Build for windows + +on: + workflow_call: + workflow_dispatch: + +jobs: + build: + name: Build + + runs-on: ${{ matrix.runner }} + + strategy: + fail-fast: false + matrix: + include: + - arch: x64 + runner: windows-latest + - arch: ia32 + runner: windows-latest + # The support of windows-arm64 dart-sdk is in beta. + # TODO: Enable this once windows-arm64 support is stable. + # - arch: arm64 + # runner: windows-latest + + steps: + - uses: actions/checkout@v4 + + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Build + run: dart run grinder pkg-standalone-windows-${{ matrix.arch }} + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: build-windows-${{ matrix.arch }} + path: build/*.zip + if-no-files-found: error + compression-level: 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e55102c9..cabc84e11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,14 +1,5 @@ name: CI -defaults: - run: {shell: bash} - -# The default Node version lives in ../util/initialize/action.yml. It should be -# kept up-to-date with the latest Node LTS releases, along with the various -# node-version matrices below. -# -# Next update: April 2021 - on: push: branches: [main, feature.*] @@ -16,317 +7,15 @@ on: pull_request: jobs: - format: - name: Code formatting - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: dart-lang/setup-dart@v1 - - run: dart format --fix . - - run: git diff --exit-code - - static_analysis: - name: Static analysis - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Analyze Dart - run: dart analyze --fatal-warnings ./ - - dartdoc: - name: Dartdoc - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: dartdoc sass - run: dart run dartdoc --quiet --no-generate-docs - --errors ambiguous-doc-reference,broken-link,deprecated - --errors unknown-directive,unknown-macro,unresolved-doc-reference - - name: dartdoc sass_api - run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs - --errors ambiguous-doc-reference,broken-link,deprecated - --errors unknown-directive,unknown-macro,unresolved-doc-reference - - sass_spec_language: - name: "Language Tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.async_label }}" - runs-on: ubuntu-latest - - strategy: - fail-fast: false - matrix: - dart_channel: [stable, dev] - async_label: [synchronous] - async_args: [''] - include: - - dart_channel: stable - async_label: asynchronous - async_args: '--cmd-args --async' - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: - dart-sdk: ${{ matrix.dart_channel }} - github-token: ${{ github.token }} - - uses: ./.github/util/sass-spec - - - name: Run specs - run: npm run sass-spec -- --dart .. $extra_args - working-directory: sass-spec - env: {extra_args: "${{ matrix.async_args }}"} - - # The versions should be kept up-to-date with the latest LTS Node releases. - # They next need to be rotated April 2021. See - # https://github.com/nodejs/Release. - sass_spec_js: - name: "JS API Tests | Pure JS | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}" - runs-on: "${{ matrix.os }}" - - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - dart_channel: [stable] - node-version: [18] - include: - # Include LTS versions on Ubuntu - - os: ubuntu-latest - dart_channel: stable - node-version: 16 - - os: ubuntu-latest - dart_channel: stable - node-version: 14 - - os: ubuntu-latest - dart_channel: dev - node-version: 18 - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: - dart-sdk: ${{ matrix.dart_channel }} - github-token: ${{ github.token }} - node-version: ${{ matrix.node-version }} - - uses: ./.github/util/sass-spec - - - name: Build JS - run: dart run grinder pkg-npm-dev - - - name: Check out Sass specification - uses: sass/clone-linked-repo@v1 - with: - repo: sass/sass - path: language - - - name: Run tests - run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm - working-directory: sass-spec - - # The versions should be kept up-to-date with the latest LTS Node releases. - # They next need to be rotated October 2021. See - # https://github.com/nodejs/Release. - sass_spec_js_embedded: - name: 'JS API Tests | Embedded | Node ${{ matrix.node-version }} | ${{ matrix.os }}' - runs-on: ${{ matrix.os }}-latest - if: "github.event_name != 'pull_request' || !contains(github.event.pull_request.body, 'skip sass-embedded')" - - strategy: - fail-fast: false - matrix: - os: [ubuntu, windows, macos] - node-version: [18] - include: - # Include LTS versions on Ubuntu - - os: ubuntu - node-version: 16 - - os: ubuntu - node-version: 14 - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: - github-token: ${{ github.token }} - node-version: ${{ matrix.node-version }} - - uses: ./.github/util/sass-spec - - - name: Check out the embedded host - uses: sass/clone-linked-repo@v1 - with: {repo: sass/embedded-host-node} - - - name: Check out the language repo - uses: sass/clone-linked-repo@v1 - with: {repo: sass/sass, path: build/language} - - - name: Initialize embedded host - run: | - npm install - npm run init -- --compiler-path=.. --language-path=../build/language - npm run compile - mv {`pwd`/,dist/}lib/src/vendor/dart-sass - working-directory: embedded-host-node - - - name: Version info - run: | - path=embedded-host-node/dist/lib/src/vendor/dart-sass/sass - if [[ -f "$path.cmd" ]]; then "./$path.cmd" --version - elif [[ -f "$path.bat" ]]; then "./$path.bat" --version - elif [[ -f "$path.exe" ]]; then "./$path.exe" --version - else "./$path" --version - fi - - - name: Run tests - run: npm run js-api-spec -- --sassPackage ../embedded-host-node --sassSassRepo ../build/language - working-directory: sass-spec - - sass_spec_js_browser: - name: "JS API Tests | Browser | Dart ${{ matrix.dart_channel }}" - - strategy: - matrix: - dart_channel: [stable] - fail-fast: false - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: browser-actions/setup-chrome@v1 - - uses: ./.github/util/initialize - with: - dart-sdk: ${{ matrix.dart_channel }} - github-token: ${{ github.token }} - - uses: ./.github/util/sass-spec - - - name: Build JS - run: dart run grinder pkg-npm-dev - - - name: Install built dependencies - run: npm install - working-directory: build/npm - - - name: Check out Sass specification - uses: sass/clone-linked-repo@v1 - with: - repo: sass/sass - path: language - - - name: Run tests - run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm --browser - working-directory: sass-spec - env: - CHROME_EXECUTABLE: chrome - - dart_tests: - name: "Dart tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.os }}" - runs-on: "${{ matrix.os }}" - - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - dart_channel: [stable] - # TODO(nweiz): Re-enable this when - # https://github.com/dart-lang/sdk/issues/52121#issuecomment-1728534228 - # is addressed. - # include: [{os: ubuntu-latest, dart_channel: dev}] - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: - dart-sdk: ${{ matrix.dart_channel }} - github-token: ${{ github.token }} - - - run: dart run grinder pkg-standalone-dev - - name: Run tests - run: dart run test -x node - - # Unit tests that use Node.js, defined in test/. - # - # The versions should be kept up-to-date with the latest LTS Node releases. - # They next need to be rotated April 2021. See - # https://github.com/nodejs/Release. - node_tests: - name: "Node tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}" - runs-on: "${{ matrix.os }}" - - strategy: - fail-fast: false - - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - dart_channel: [stable] - node-version: [18] - include: - # Include LTS versions on Ubuntu - - os: ubuntu-latest - dart_channel: stable - node-version: 16 - - os: ubuntu-latest - dart_channel: stable - node-version: 14 - - os: ubuntu-latest - dart_channel: dev - node-version: 18 - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: - dart-sdk: ${{ matrix.dart_channel }} - github-token: ${{ github.token }} - node-version: ${{ matrix.node-version }} - - - run: dart run grinder pkg-npm-dev - - name: Run tests - run: dart run test -t node -j 2 - - browser_tests: - name: "Browser Tests | Dart ${{ matrix.dart_channel }}" - - strategy: - matrix: - dart_channel: [stable] - fail-fast: false - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: browser-actions/setup-chrome@v1 - - uses: ./.github/util/initialize - with: - dart-sdk: ${{ matrix.dart_channel }} - github-token: ${{ github.token }} - - - run: dart run grinder pkg-npm-dev - - name: Run tests - run: dart run test -p chrome -j 2 - env: - CHROME_EXECUTABLE: chrome + test: + uses: ./.github/workflows/test.yml + secrets: inherit double_check: name: Double-check runs-on: ubuntu-latest - needs: - - sass_spec_language - - sass_spec_js - - sass_spec_js_browser - - sass_spec_js_embedded - - dart_tests - - node_tests - - browser_tests - - static_analysis - - dartdoc - - format - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" + needs: [test] + if: "startsWith(github.ref, 'refs/tags/') && github.event.repository.fork == false" steps: - uses: actions/checkout@v4 @@ -336,325 +25,16 @@ jobs: - name: Run checks run: dart run grinder double-check-before-release - bootstrap: - name: "Bootstrap ${{ matrix.bootstrap_version }}" - runs-on: ubuntu-latest - needs: [double_check] - - strategy: - fail-fast: false - matrix: - bootstrap_version: [4, 5] - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - run: dart run grinder fetch-bootstrap${{matrix.bootstrap_version}} - env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} - - name: Build - run: dart bin/sass.dart --quiet build/bootstrap/scss:build/bootstrap-output - - bourbon: - name: Bourbon - runs-on: ubuntu-latest + test_vendor: needs: [double_check] - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - run: dart run grinder fetch-bourbon - env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} - - name: Test - run: | - dart bin/sass.dart --quiet -I build/bourbon -I build/bourbon/spec/fixtures \ - build/bourbon/spec/fixtures:build/bourbon-output - - foundation: - name: Foundation - runs-on: ubuntu-latest - needs: [double_check] - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - run: dart run grinder fetch-foundation - env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} - # TODO(nweiz): Foundation has proper Sass tests, but they're currently not - # compatible with Dart Sass. Once they are, we should run those rather - # than just building the CSS output. - - name: Build - run: dart bin/sass.dart --quiet build/foundation-sites/assets:build/foundation-output - - bulma: - name: Bulma - runs-on: ubuntu-latest - needs: [double_check] - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - 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 - - deploy_github_linux: - name: "Deploy Github: linux-ia32, linux-x64" - runs-on: ubuntu-latest - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Deploy - run: dart run grinder pkg-github-release pkg-github-linux-ia32 pkg-github-linux-x64 - env: - GH_TOKEN: "${{ secrets.GH_TOKEN }}" - GH_USER: sassbot - - deploy_github_linux_qemu: - name: "Deploy Github: linux-${{ matrix.arch }}" - runs-on: ubuntu-latest - strategy: - matrix: - include: - - arch: arm - platform: linux/arm/v7 - - arch: arm64 - platform: linux/arm64 - needs: [deploy_github_linux] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - uses: docker/setup-qemu-action@v3 - - name: Deploy - run: | - docker run --rm \ - --env "GH_TOKEN=$GH_TOKEN" \ - --env "GH_USER=$GH_USER" \ - --platform ${{ matrix.platform }} \ - --volume "$PWD:$PWD" \ - --workdir "$PWD" \ - docker.io/library/dart:latest \ - /bin/sh -c "dart pub get && dart run grinder pkg-github-linux-${{ matrix.arch }}" - env: - GH_TOKEN: "${{ secrets.GH_TOKEN }}" - GH_USER: sassbot - - deploy_github: - name: "Deploy Github: ${{ matrix.platform }}" - runs-on: ${{ matrix.runner }} - needs: [deploy_github_linux] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - strategy: - matrix: - include: - - runner: macos-latest - platform: macos-x64 - architecture: x64 - # https://github.blog/2023-10-02-introducing-the-new-apple-silicon-powered-m1-macos-larger-runner-for-github-actions/ - - runner: macos-latest-xlarge - platform: macos-arm64 - architecture: arm64 - - runner: windows-latest - platform: windows - architecture: x64 - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - # Workaround for dart-lang/setup-dart#59 - with: - github-token: ${{ github.token }} - architecture: ${{ matrix.architecture }} - - - name: Deploy - run: dart run grinder pkg-github-${{ matrix.platform }} - env: - GH_TOKEN: "${{ secrets.GH_TOKEN }}" - GH_USER: sassbot - - deploy_npm: - name: Deploy npm - runs-on: ubuntu-latest - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Deploy - run: dart run grinder pkg-npm-deploy - env: - NPM_TOKEN: "${{ secrets.NPM_TOKEN }}" - - deploy_bazel: - name: Deploy Bazel - runs-on: ubuntu-latest - needs: [deploy_npm] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Deploy - run: dart run grinder update-bazel - env: - GH_TOKEN: "${{ secrets.GH_TOKEN }}" - GH_USER: sassbot - - deploy_pub: - name: "Deploy Pub" - runs-on: ubuntu-latest - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Deploy - run: dart run grinder protobuf pkg-pub-deploy - env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} - - deploy_sub_packages: - name: "Deploy Sub-Packages" - runs-on: ubuntu-latest - needs: [deploy_pub] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Deploy - run: dart run grinder deploy-sub-packages - env: - PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}" - GH_TOKEN: "${{ secrets.GH_TOKEN }}" - GH_USER: sassbot - - deploy_homebrew: - name: "Deploy Homebrew" - runs-on: ubuntu-latest - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: dart-lang/setup-dart@v1 - - run: dart pub get - - - name: Deploy - run: dart run grinder pkg-homebrew-update - env: - GH_TOKEN: "${{ secrets.GH_TOKEN }}" - GH_USER: sassbot - - deploy_chocolatey: - name: "Deploy Chocolatey" - runs-on: windows-latest - needs: [bootstrap, bourbon, foundation, bulma] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - - steps: - - uses: actions/checkout@v4 - - uses: ./.github/util/initialize - with: {github-token: "${{ github.token }}"} - - - name: Deploy - run: dart run grinder pkg-chocolatey-deploy - env: {CHOCOLATEY_TOKEN: "${{ secrets.CHOCOLATEY_TOKEN }}"} - - deploy_website: - name: "Deploy sass-lang.com" - runs-on: ubuntu-latest - needs: [deploy_npm] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - steps: - - uses: actions/checkout@v4 - with: - repository: sass/sass-site - token: ${{ secrets.SASS_SITE_TOKEN }} - - - name: Get version - id: version - run: echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT" - - - name: Update Dart Sass version - run: npm install sass@${{ steps.version.outputs.version }} - - - uses: EndBug/add-and-commit@v9 - with: - author_name: Sass Bot - author_email: sass.bot.beep.boop@gmail.com - message: Cut a release for a new Dart Sass version - commit: --allow-empty - - release_embedded_host: - name: "Release Embedded Host" - runs-on: ubuntu-latest - needs: [deploy_github_linux, deploy_github_linux_qemu, deploy_github] - if: "startsWith(github.ref, 'refs/tags/') && github.repository == 'sass/dart-sass'" - steps: - - uses: actions/checkout@v4 - with: - repository: sass/embedded-host-node - token: ${{ secrets.GH_TOKEN }} - - - name: Get version - id: version - run: | - echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT" - echo "protocol_version=$(curl -fsSL -H "Authorization: Bearer ${{ github.token }}" https://raw.githubusercontent.com/sass/sass/HEAD/spec/EMBEDDED_PROTOCOL_VERSION)" | tee --append "$GITHUB_OUTPUT" - - - name: Update version - run: | - # Update binary package versions - for dir in $(ls npm); do - cat "npm/$dir/package.json" | - jq --arg version ${{ steps.version.outputs.version }} ' - .version |= $version - ' > package.json.tmp && - mv package.json.tmp "npm/$dir/package.json" - done - - # Update main package version and dependencies on binary packages - cat package.json | - jq --arg version ${{ steps.version.outputs.version }} --arg protocol_version ${{ steps.version.outputs.protocol_version }} ' - .version |= $version | - ."compiler-version" |= $version | - ."protocol-version" |= $protocol_version | - .optionalDependencies = (.optionalDependencies | .[] |= $version) - ' > package.json.tmp && - mv package.json.tmp package.json - curl -fsSL -H "Authorization: Bearer ${{ github.token }}" https://raw.githubusercontent.com/sass/dart-sass/${{ steps.version.outputs.version }}/CHANGELOG.md > CHANGELOG.md - shell: bash - - - uses: EndBug/add-and-commit@v9 - with: - author_name: Sass Bot - author_email: sass.bot.beep.boop@gmail.com - message: Update Dart Sass version and release - tag: ${{ steps.version.outputs.version }} + if: "startsWith(github.ref, 'refs/tags/') && github.event.repository.fork == false" + uses: ./.github/workflows/test-vendor.yml + secrets: inherit + + release: + needs: [test_vendor] + if: "startsWith(github.ref, 'refs/tags/') && github.event.repository.fork == false" + permissions: + contents: write + uses: ./.github/workflows/release.yml + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 000000000..43d32c0e4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,219 @@ +name: Release + +on: + workflow_call: + +jobs: + build_android: + uses: ./.github/workflows/build-android.yml + secrets: inherit + + build_linux: + uses: ./.github/workflows/build-linux.yml + secrets: inherit + + build_linux_musl: + uses: ./.github/workflows/build-linux-musl.yml + secrets: inherit + + build_macos: + uses: ./.github/workflows/build-macos.yml + secrets: inherit + + build_windows: + uses: ./.github/workflows/build-windows.yml + secrets: inherit + + release_github: + name: Release Github + runs-on: ubuntu-latest + needs: [build_android, build_linux, build_linux_musl, build_macos, build_windows] + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Deploy + run: dart run grinder pkg-github-release + env: + GH_TOKEN: "${{ secrets.GH_TOKEN }}" + GH_USER: sassbot + + deploy_github: + name: Deploy Github + runs-on: ubuntu-latest + needs: [release_github] + + permissions: + contents: write + + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + + - name: Release + uses: softprops/action-gh-release@v1 + with: + files: | + build-*/* + + deploy_npm: + name: Deploy npm + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Deploy + run: dart run grinder pkg-npm-deploy + env: + NPM_TOKEN: "${{ secrets.NPM_TOKEN }}" + + deploy_bazel: + name: Deploy Bazel + runs-on: ubuntu-latest + needs: [deploy_npm] + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Deploy + run: dart run grinder update-bazel + env: + GH_TOKEN: "${{ secrets.GH_TOKEN }}" + GH_USER: sassbot + + deploy_pub: + name: Deploy Pub + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Deploy + run: dart run grinder protobuf pkg-pub-deploy + env: {PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}"} + + deploy_sub_packages: + name: Deploy Sub-Packages + runs-on: ubuntu-latest + needs: [deploy_pub] + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Deploy + run: dart run grinder deploy-sub-packages + env: + PUB_CREDENTIALS: "${{ secrets.PUB_CREDENTIALS }}" + GH_TOKEN: "${{ secrets.GH_TOKEN }}" + GH_USER: sassbot + + deploy_homebrew: + name: Deploy Homebrew + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + - run: dart pub get + + - name: Deploy + run: dart run grinder pkg-homebrew-update + env: + GH_TOKEN: "${{ secrets.GH_TOKEN }}" + GH_USER: sassbot + + deploy_chocolatey: + name: Deploy Chocolatey + runs-on: windows-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Deploy + run: dart run grinder pkg-chocolatey-deploy + env: {CHOCOLATEY_TOKEN: "${{ secrets.CHOCOLATEY_TOKEN }}"} + + deploy_website: + name: Deploy sass-lang.com + runs-on: ubuntu-latest + needs: [deploy_npm] + + steps: + - uses: actions/checkout@v4 + with: + repository: sass/sass-site + token: ${{ secrets.SASS_SITE_TOKEN }} + + - name: Get version + id: version + run: echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT" + + - name: Update Dart Sass version + run: npm install sass@${{ steps.version.outputs.version }} + + - uses: EndBug/add-and-commit@v9 + with: + author_name: Sass Bot + author_email: sass.bot.beep.boop@gmail.com + message: Cut a release for a new Dart Sass version + commit: --allow-empty + + release_embedded_host: + name: Release Embedded Host + runs-on: ubuntu-latest + needs: [deploy_github] + + steps: + - uses: actions/checkout@v4 + with: + repository: sass/embedded-host-node + token: ${{ secrets.GH_TOKEN }} + + - name: Get version + id: version + run: | + echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT" + echo "protocol_version=$(curl -fsSL -H "Authorization: Bearer ${{ github.token }}" https://raw.githubusercontent.com/sass/sass/HEAD/spec/EMBEDDED_PROTOCOL_VERSION)" | tee --append "$GITHUB_OUTPUT" + + - name: Update version + run: | + # Update binary package versions + for dir in $(ls npm); do + cat "npm/$dir/package.json" | + jq --arg version ${{ steps.version.outputs.version }} ' + .version |= $version + ' > package.json.tmp && + mv package.json.tmp "npm/$dir/package.json" + done + + # Update main package version and dependencies on binary packages + cat package.json | + jq --arg version ${{ steps.version.outputs.version }} --arg protocol_version ${{ steps.version.outputs.protocol_version }} ' + .version |= $version | + ."compiler-version" |= $version | + ."protocol-version" |= $protocol_version | + .optionalDependencies = (.optionalDependencies | .[] |= $version) + ' > package.json.tmp && + mv package.json.tmp package.json + curl -fsSL -H "Authorization: Bearer ${{ github.token }}" https://raw.githubusercontent.com/sass/dart-sass/${{ steps.version.outputs.version }}/CHANGELOG.md > CHANGELOG.md + shell: bash + + - uses: EndBug/add-and-commit@v9 + with: + author_name: Sass Bot + author_email: sass.bot.beep.boop@gmail.com + message: Update Dart Sass version and release + tag: ${{ steps.version.outputs.version }} diff --git a/.github/workflows/test-vendor.yml b/.github/workflows/test-vendor.yml new file mode 100644 index 000000000..e2d51825e --- /dev/null +++ b/.github/workflows/test-vendor.yml @@ -0,0 +1,72 @@ +name: Test Vendor + +on: + workflow_call: + workflow_dispatch: + +jobs: + bootstrap: + name: "Bootstrap ${{ matrix.bootstrap_version }}" + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + bootstrap_version: [4, 5] + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - run: dart run grinder fetch-bootstrap${{matrix.bootstrap_version}} + env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} + - name: Build + run: dart bin/sass.dart --quiet build/bootstrap/scss:build/bootstrap-output + + bourbon: + name: Bourbon + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - run: dart run grinder fetch-bourbon + env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} + - name: Test + run: | + dart bin/sass.dart --quiet -I build/bourbon -I build/bourbon/spec/fixtures \ + build/bourbon/spec/fixtures:build/bourbon-output + + foundation: + name: Foundation + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - run: dart run grinder fetch-foundation + env: {GITHUB_BEARER_TOKEN: "${{ secrets.GITHUB_TOKEN }}"} + # TODO(nweiz): Foundation has proper Sass tests, but they're currently not + # compatible with Dart Sass. Once they are, we should run those rather + # than just building the CSS output. + - name: Build + run: dart bin/sass.dart --quiet build/foundation-sites/assets:build/foundation-output + + bulma: + name: Bulma + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - 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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 000000000..aa31b610e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,309 @@ +name: Test + +defaults: + run: {shell: bash} + +on: + workflow_call: + workflow_dispatch: + +jobs: + format: + name: Code formatting + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: dart-lang/setup-dart@v1 + - run: dart format --fix . + - run: git diff --exit-code + + static_analysis: + name: Static analysis + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: Analyze Dart + run: dart analyze --fatal-warnings ./ + + dartdoc: + name: Dartdoc + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: {github-token: "${{ github.token }}"} + + - name: dartdoc sass + run: dart run dartdoc --quiet --no-generate-docs + --errors ambiguous-doc-reference,broken-link,deprecated + --errors unknown-directive,unknown-macro,unresolved-doc-reference + - name: dartdoc sass_api + run: cd pkg/sass_api && dart run dartdoc --quiet --no-generate-docs + --errors ambiguous-doc-reference,broken-link,deprecated + --errors unknown-directive,unknown-macro,unresolved-doc-reference + + sass_spec_language: + name: "Language Tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.async_label }}" + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + dart_channel: [stable, dev] + async_label: [synchronous] + async_args: [''] + include: + - dart_channel: stable + async_label: asynchronous + async_args: '--cmd-args --async' + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: + dart-sdk: ${{ matrix.dart_channel }} + github-token: ${{ github.token }} + - uses: ./.github/util/sass-spec + + - name: Run specs + run: npm run sass-spec -- --dart .. $extra_args + working-directory: sass-spec + env: {extra_args: "${{ matrix.async_args }}"} + + sass_spec_js: + name: "JS API Tests | Pure JS | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}" + runs-on: "${{ matrix.os }}" + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + dart_channel: [stable] + node-version: ['lts/*'] + include: + # Test older LTS versions + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-1 + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-2 + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-3 + # Test LTS version with dart dev channel + - os: ubuntu-latest + dart_channel: dev + node-version: 'lts/*' + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: + dart-sdk: ${{ matrix.dart_channel }} + github-token: ${{ github.token }} + node-version: ${{ matrix.node-version }} + - uses: ./.github/util/sass-spec + + - name: Build JS + run: dart run grinder pkg-npm-dev + + - name: Check out Sass specification + uses: sass/clone-linked-repo@v1 + with: + repo: sass/sass + path: language + + - name: Run tests + run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm + working-directory: sass-spec + + sass_spec_js_embedded: + name: 'JS API Tests | Embedded | Node ${{ matrix.node-version }} | ${{ matrix.os }}' + runs-on: ${{ matrix.os }} + if: "github.event_name != 'pull_request' || !contains(github.event.pull_request.body, 'skip sass-embedded')" + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: ['lts/*'] + include: + # Test older LTS versions + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-1 + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-2 + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-3 + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: + github-token: ${{ github.token }} + node-version: ${{ matrix.node-version }} + - uses: ./.github/util/sass-spec + + - name: Check out the embedded host + uses: sass/clone-linked-repo@v1 + with: {repo: sass/embedded-host-node} + + - name: Check out the language repo + uses: sass/clone-linked-repo@v1 + with: {repo: sass/sass, path: build/language} + + - name: Initialize embedded host + run: | + npm install + npm run init -- --compiler-path=.. --language-path=../build/language + npm run compile + mv {`pwd`/,dist/}lib/src/vendor/dart-sass + working-directory: embedded-host-node + + - name: Version info + run: | + path=embedded-host-node/dist/lib/src/vendor/dart-sass/sass + if [[ -f "$path.cmd" ]]; then "./$path.cmd" --version + elif [[ -f "$path.bat" ]]; then "./$path.bat" --version + elif [[ -f "$path.exe" ]]; then "./$path.exe" --version + else "./$path" --version + fi + + - name: Run tests + run: npm run js-api-spec -- --sassPackage ../embedded-host-node --sassSassRepo ../build/language + working-directory: sass-spec + + sass_spec_js_browser: + name: "JS API Tests | Browser | Dart ${{ matrix.dart_channel }}" + + strategy: + matrix: + dart_channel: [stable] + fail-fast: false + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: browser-actions/setup-chrome@v1 + - uses: ./.github/util/initialize + with: + dart-sdk: ${{ matrix.dart_channel }} + github-token: ${{ github.token }} + - uses: ./.github/util/sass-spec + + - name: Build JS + run: dart run grinder pkg-npm-dev + + - name: Install built dependencies + run: npm install + working-directory: build/npm + + - name: Check out Sass specification + uses: sass/clone-linked-repo@v1 + with: + repo: sass/sass + path: language + + - name: Run tests + run: npm run js-api-spec -- --sassSassRepo ../language --sassPackage ../build/npm --browser + working-directory: sass-spec + env: + CHROME_EXECUTABLE: chrome + + dart_tests: + name: "Dart tests | Dart ${{ matrix.dart_channel }} | ${{ matrix.os }}" + runs-on: "${{ matrix.os }}" + + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + dart_channel: [stable] + # TODO(nweiz): Re-enable this when + # https://github.com/dart-lang/sdk/issues/52121#issuecomment-1728534228 + # is addressed. + # include: [{os: ubuntu-latest, dart_channel: dev}] + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: + dart-sdk: ${{ matrix.dart_channel }} + github-token: ${{ github.token }} + + - run: dart run grinder pkg-standalone-dev + - name: Run tests + run: dart run test -x node + + # Unit tests that use Node.js, defined in test/. + node_tests: + name: "Node tests | Dart ${{ matrix.dart_channel }} | Node ${{ matrix.node-version }} | ${{ matrix.os }}" + runs-on: "${{ matrix.os }}" + + strategy: + fail-fast: false + + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + dart_channel: [stable] + node-version: ['lts/*'] + include: + # Test older LTS versions + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-1 + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-2 + - os: ubuntu-latest + dart_channel: stable + node-version: lts/-3 + # Test LTS version with dart dev channel + - os: ubuntu-latest + dart_channel: dev + node-version: 'lts/*' + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/util/initialize + with: + dart-sdk: ${{ matrix.dart_channel }} + github-token: ${{ github.token }} + node-version: ${{ matrix.node-version }} + + - run: dart run grinder pkg-npm-dev + - name: Run tests + run: dart run test -t node -j 2 + + browser_tests: + name: "Browser Tests | Dart ${{ matrix.dart_channel }}" + + strategy: + matrix: + dart_channel: [stable] + fail-fast: false + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: browser-actions/setup-chrome@v1 + - uses: ./.github/util/initialize + with: + dart-sdk: ${{ matrix.dart_channel }} + github-token: ${{ github.token }} + + - run: dart run grinder pkg-npm-dev + - name: Run tests + run: dart run test -p chrome -j 2 + env: + CHROME_EXECUTABLE: chrome diff --git a/CHANGELOG.md b/CHANGELOG.md index a4995a0cb..5f520294c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ points that aren't in the US-ASCII region. Previously only code points U+0100 LATIN CAPITAL LETTER A WITH MACRON were escaped. +* Provide official releases for musl LibC and for Android. + ### JS API * Fix a bug where certain exceptions could produce `SourceSpan`s that didn't From f3c7be5affba170655fd5190a2bf9f0c53c99f08 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 28 Dec 2023 15:10:23 -0800 Subject: [PATCH 08/35] Make meta.apply() an AsyncBuiltInCallable (#2152) Closes #2151 --- CHANGELOG.md | 2 ++ lib/src/visitor/async_evaluate.dart | 3 ++- lib/src/visitor/evaluate.dart | 2 +- pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 6 files changed, 12 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f520294c..7ee7dd522 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ * Provide official releases for musl LibC and for Android. +* Don't crash when running `meta.apply()` in asynchronous mode. + ### JS API * Fix a bug where certain exceptions could produce `SourceSpan`s that didn't diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 767d2393b..7d75430f7 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -545,7 +545,8 @@ final class _EvaluateVisitor namesInErrors: true); _assertConfigurationIsEmpty(configuration, nameInError: true); }, url: "sass:meta"), - BuiltInCallable.mixin("apply", r"$mixin, $args...", (arguments) async { + AsyncBuiltInCallable.mixin("apply", r"$mixin, $args...", + (arguments) async { var mixin = arguments[0]; var args = arguments[1] as SassArgumentList; diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 50b2cce78..37c2e0c08 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: 58ef9912c6a9d9cfe9c3f5d991f625ab1a627e7a +// Checksum: 71dcf1747eb45036d3c1b5b57bd0cd5dbe6b8e14 // // ignore_for_file: unused_import diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 6dd5a42b9..d1b3b19b5 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.2.6 + +* No user-visible changes. + ## 9.2.5 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 4756d33f1..3984e8f17 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: 9.2.5 +version: 9.2.6 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.69.5 + sass: 1.69.6 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index c42d2133e..f87c516ce 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.69.6-dev +version: 1.69.6 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From 6205eac5d149b21154ca044ebfc0cd55631346d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Thu, 28 Dec 2023 16:53:58 -0800 Subject: [PATCH 09/35] Add wait time before update website (#2153) --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 43d32c0e4..b374f5995 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,6 +161,9 @@ jobs: id: version run: echo "version=${GITHUB_REF##*/}" | tee --append "$GITHUB_OUTPUT" + - name: Wait for npm registry's CDN to catch up on replications + run: sleep 600 + - name: Update Dart Sass version run: npm install sass@${{ steps.version.outputs.version }} From 006baa5642e6c0ff8bb41da8f914359e06fc04d4 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 2 Jan 2024 13:52:51 -0800 Subject: [PATCH 10/35] Update the pubspec and changelog for sass/embedded-host-node#266 (#2158) --- CHANGELOG.md | 7 +++++++ pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ee7dd522..a7884687c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.69.7 + +### Embedded Sass + +* In the JS Embedded Host, properly install the x64 Dart Sass executable on + ARM64 Windows. + ## 1.69.6 * Produce better output for numbers with complex units in `meta.inspect()` and diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index d1b3b19b5..8faa204e3 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.2.7 + +* No user-visible changes. + ## 9.2.6 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 3984e8f17..c2bc48591 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: 9.2.6 +version: 9.2.7 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.69.6 + sass: 1.69.7 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index f87c516ce..013259037 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.69.6 +version: 1.69.7 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From b263a7260801a8a0bc2b87d35cf584ba4a6eca20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 16 Jan 2024 14:21:23 -0800 Subject: [PATCH 11/35] Use implementation name dart-sass for VersionResponse (#2156) Co-authored-by: Natalie Weizenbaum --- CHANGELOG.md | 7 +++++++ lib/src/embedded/isolate_dispatcher.dart | 2 +- pubspec.yaml | 2 +- test/embedded/protocol_test.dart | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7884687c..5cb089502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.69.8 + +### Embedded Sass + +* The Dart Sass embedded compiler now reports its name as "dart-sass" rather + than "Dart Sass", to match the JS API's `info` field. + ## 1.69.7 ### Embedded Sass diff --git a/lib/src/embedded/isolate_dispatcher.dart b/lib/src/embedded/isolate_dispatcher.dart index 018321cdd..b208cc6b3 100644 --- a/lib/src/embedded/isolate_dispatcher.dart +++ b/lib/src/embedded/isolate_dispatcher.dart @@ -137,7 +137,7 @@ class IsolateDispatcher { ..protocolVersion = const String.fromEnvironment("protocol-version") ..compilerVersion = const String.fromEnvironment("compiler-version") ..implementationVersion = const String.fromEnvironment("compiler-version") - ..implementationName = "Dart Sass"; + ..implementationName = "dart-sass"; } /// Handles an error thrown by the dispatcher or code it dispatches to. diff --git a/pubspec.yaml b/pubspec.yaml index 013259037..2722f7bcc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.69.7 +version: 1.69.8-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass diff --git a/test/embedded/protocol_test.dart b/test/embedded/protocol_test.dart index 9e293365e..b77d376e0 100644 --- a/test/embedded/protocol_test.dart +++ b/test/embedded/protocol_test.dart @@ -96,7 +96,7 @@ void main() { Version.parse(response.protocolVersion); // shouldn't throw Version.parse(response.compilerVersion); // shouldn't throw Version.parse(response.implementationVersion); // shouldn't throw - expect(response.implementationName, equals("Dart Sass")); + expect(response.implementationName, equals("dart-sass")); await process.close(); }); From 0d91c92dfbc5b570161f9fc554c2e90f0f220567 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 16 Jan 2024 15:49:24 -0800 Subject: [PATCH 12/35] Support CompileRequest.silent of embedded protocol (#2160) Co-authored-by: Natalie Weizenbaum --- CHANGELOG.md | 3 +++ lib/src/embedded/compilation_dispatcher.dart | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cb089502..90117468e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ### Embedded Sass +* Support the `CompileRequest.silent` field. This allows compilations with no + logging to avoid unnecessary request/response cycles. + * The Dart Sass embedded compiler now reports its name as "dart-sass" rather than "Dart Sass", to match the JS API's `info` field. diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 218ea6f18..4e390fcd9 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -12,6 +12,7 @@ import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; import 'package:sass/sass.dart' as sass; +import '../logger.dart'; import '../value/function.dart'; import '../value/mixin.dart'; import 'embedded_sass.pb.dart'; @@ -117,8 +118,10 @@ final class CompilationDispatcher { var style = request.style == OutputStyle.COMPRESSED ? sass.OutputStyle.compressed : sass.OutputStyle.expanded; - var logger = EmbeddedLogger(this, - color: request.alertColor, ascii: request.alertAscii); + var logger = request.silent + ? Logger.quiet + : EmbeddedLogger(this, + color: request.alertColor, ascii: request.alertAscii); try { var importers = request.importers.map((importer) => From 076414d3e793ab907591b34fc8a3f694529277d4 Mon Sep 17 00:00:00 2001 From: Ed Rivas Date: Thu, 18 Jan 2024 02:48:21 +0000 Subject: [PATCH 13/35] [Shared Resources] dart-sass implementation (#2134) Co-authored-by: James Stuckey Weber Co-authored-by: Jonny Gerig Meyer --- CHANGELOG.md | 16 +++++- lib/src/js.dart | 6 ++ lib/src/js/compiler.dart | 113 ++++++++++++++++++++++++++++++++++++++ lib/src/js/exports.dart | 4 ++ pkg/sass_api/CHANGELOG.md | 4 ++ pkg/sass_api/pubspec.yaml | 4 +- pubspec.yaml | 2 +- tool/grind.dart | 4 ++ 8 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 lib/src/js/compiler.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index 90117468e..7374727f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,18 @@ -## 1.69.8 +## 1.70.0 + +### JavaScript API + +* Add a `sass.initCompiler()` function that returns a `sass.Compiler` object + which supports `compile()` and `compileString()` methods with the same API as + the global Sass object. On the Node.js embedded host, each `sass.Compiler` + object uses a single long-lived subprocess, making compiling multiple + stylesheets much more efficient. + +* Add a `sass.initAsyncCompiler()` function that returns a `sass.AsyncCompiler` + object which supports `compileAsync()` and `compileStringAsync()` methods with + the same API as the global Sass object. On the Node.js embedded host, each + `sass.AsynCompiler` object uses a single long-lived subprocess, making + compiling multiple stylesheets much more efficient. ### Embedded Sass diff --git a/lib/src/js.dart b/lib/src/js.dart index 9a2c51c06..92ab23f66 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -5,6 +5,7 @@ import 'js/exception.dart'; import 'js/exports.dart'; import 'js/compile.dart'; +import 'js/compiler.dart'; import 'js/legacy.dart'; import 'js/legacy/types.dart'; import 'js/legacy/value.dart'; @@ -24,6 +25,11 @@ void main() { exports.compileAsync = allowInteropNamed('sass.compileAsync', compileAsync); exports.compileStringAsync = allowInteropNamed('sass.compileStringAsync', compileStringAsync); + exports.initCompiler = allowInteropNamed('sass.initCompiler', initCompiler); + exports.initAsyncCompiler = + allowInteropNamed('sass.initAsyncCompiler', initAsyncCompiler); + exports.Compiler = compilerClass; + exports.AsyncCompiler = asyncCompilerClass; exports.Value = valueClass; exports.SassBoolean = booleanClass; exports.SassArgumentList = argumentListClass; diff --git a/lib/src/js/compiler.dart b/lib/src/js/compiler.dart new file mode 100644 index 000000000..ab1886b3f --- /dev/null +++ b/lib/src/js/compiler.dart @@ -0,0 +1,113 @@ +// Copyright 2024 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 'dart:js_util'; + +import 'package:async/async.dart'; +import 'package:node_interop/js.dart'; + +import 'compile.dart'; +import 'compile_options.dart'; +import 'reflection.dart'; +import 'utils.dart'; + +/// The Dart Compiler class. +class Compiler { + /// A flag signifying whether the instance has been disposed. + bool _disposed = false; + + /// Checks if `dispose()` has been called on this instance, and throws an + /// error if it has. Used to verify that compilation methods are not called + /// after disposal. + void _throwIfDisposed() { + if (_disposed) { + jsThrow(JsError('Compiler has already been disposed.')); + } + } +} + +/// The Dart Async Compiler class. +class AsyncCompiler extends Compiler { + /// A set of all compilations, tracked to ensure all compilations complete + /// before async disposal resolves. + final FutureGroup compilations = FutureGroup(); + + /// Adds a compilation to the FutureGroup. + void addCompilation(Promise compilation) { + Future comp = promiseToFuture(compilation); + var wrappedComp = comp.catchError((err) { + /// Ignore errors so FutureGroup doesn't close when a compilation fails. + }); + compilations.add(wrappedComp); + } +} + +/// The JavaScript `Compiler` class. +final JSClass compilerClass = () { + var jsClass = createJSClass( + 'sass.Compiler', + (Object self) => { + jsThrow(JsError(("Compiler can not be directly constructed. " + "Please use `sass.initCompiler()` instead."))) + }); + + jsClass.defineMethods({ + 'compile': (Compiler self, String path, [CompileOptions? options]) { + self._throwIfDisposed(); + return compile(path, options); + }, + 'compileString': (Compiler self, String source, + [CompileStringOptions? options]) { + self._throwIfDisposed(); + return compileString(source, options); + }, + 'dispose': (Compiler self) { + self._disposed = true; + }, + }); + + getJSClass(Compiler()).injectSuperclass(jsClass); + return jsClass; +}(); + +Compiler initCompiler() => Compiler(); + +/// The JavaScript `AsyncCompiler` class. +final JSClass asyncCompilerClass = () { + var jsClass = createJSClass( + 'sass.AsyncCompiler', + (Object self) => { + jsThrow(JsError(("AsyncCompiler can not be directly constructed. " + "Please use `sass.initAsyncCompiler()` instead."))) + }); + + jsClass.defineMethods({ + 'compileAsync': (AsyncCompiler self, String path, + [CompileOptions? options]) { + self._throwIfDisposed(); + var compilation = compileAsync(path, options); + self.addCompilation(compilation); + return compilation; + }, + 'compileStringAsync': (AsyncCompiler self, String source, + [CompileStringOptions? options]) { + self._throwIfDisposed(); + var compilation = compileStringAsync(source, options); + self.addCompilation(compilation); + return compilation; + }, + 'dispose': (AsyncCompiler self) { + self._disposed = true; + return futureToPromise((() async { + self.compilations.close(); + await self.compilations.future; + })()); + } + }); + + getJSClass(AsyncCompiler()).injectSuperclass(jsClass); + return jsClass; +}(); + +Promise initAsyncCompiler() => futureToPromise((() async => AsyncCompiler())()); diff --git a/lib/src/js/exports.dart b/lib/src/js/exports.dart index ee5a74471..3cf5bb7a5 100644 --- a/lib/src/js/exports.dart +++ b/lib/src/js/exports.dart @@ -19,6 +19,10 @@ class Exports { external set compileStringAsync(Function function); external set compile(Function function); external set compileAsync(Function function); + external set initCompiler(Function function); + external set initAsyncCompiler(Function function); + external set Compiler(JSClass function); + external set AsyncCompiler(JSClass function); external set info(String info); external set Exception(JSClass function); external set Logger(LoggerNamespace namespace); diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 8faa204e3..99a7689e4 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.3.0 + +* No user-visible changes. + ## 9.2.7 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index c2bc48591..5158fc4e4 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: 9.2.7 +version: 9.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.69.7 + sass: 1.70.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 2722f7bcc..f14d6b822 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.69.8-dev +version: 1.70.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass diff --git a/tool/grind.dart b/tool/grind.dart index 425730c03..92f919deb 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -51,6 +51,10 @@ void main(List args) { 'compileAsync', 'compileString', 'compileStringAsync', + 'initCompiler', + 'initAsyncCompiler', + 'Compiler', + 'AsyncCompiler', 'Logger', 'SassArgumentList', 'SassBoolean', From bbf97b4fb4be10b47a6045db9dcaeb3d3e9c2aa6 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 19 Jan 2024 13:38:09 -0800 Subject: [PATCH 14/35] Remove the sass dependency from package.json (#2162) This was accidentally added for local testing. --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 531856ade..823058eef 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,5 @@ "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "intercept-stdout": "^0.1.2" - }, - "dependencies": { - "sass": "^1.63.5" } } From 9423aa53ae599999f514984fe0d773558aef2be3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Thu, 1 Feb 2024 15:47:49 -0800 Subject: [PATCH 15/35] Use macos-14 runner instead of macos-latest-xlarge runner (#2167) --- .github/workflows/build-macos.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 7d31257bc..3dff23d3b 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -15,10 +15,9 @@ jobs: matrix: include: - arch: x64 - runner: macos-latest - # https://github.blog/2023-10-02-introducing-the-new-apple-silicon-powered-m1-macos-larger-runner-for-github-actions/ + runner: macos-13 - arch: arm64 - runner: macos-latest-xlarge + runner: macos-14 steps: - uses: actions/checkout@v4 From 9ee5408211f1f2fc3c34e280552f3c3afd899156 Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Tue, 6 Feb 2024 17:58:41 -0500 Subject: [PATCH 16/35] [Package Importer] Dart Implementation (#2130) Co-authored-by: Jonny Gerig Meyer --- lib/src/async_compile.dart | 9 +- lib/src/async_import_cache.dart | 6 + lib/src/compile.dart | 11 +- lib/src/embedded/compilation_dispatcher.dart | 5 + lib/src/embedded/importer/file.dart | 12 +- lib/src/executable/compile_stylesheet.dart | 6 +- lib/src/executable/repl.dart | 2 +- lib/src/executable/watch.dart | 6 +- lib/src/import_cache.dart | 8 +- lib/src/importer/filesystem.dart | 3 + lib/src/importer/js_to_dart/async_file.dart | 16 +- lib/src/importer/js_to_dart/file.dart | 16 +- lib/src/importer/node_package.dart | 387 +++++++++++++++++++ lib/src/importer/package.dart | 17 +- lib/src/js.dart | 1 + lib/src/js/compile.dart | 20 + lib/src/js/exports.dart | 1 + lib/src/js/legacy.dart | 24 ++ lib/src/js/legacy/render_options.dart | 3 + lib/src/js/utils.dart | 4 + lib/src/visitor/async_evaluate.dart | 11 +- lib/src/visitor/evaluate.dart | 11 +- pubspec.yaml | 2 +- 23 files changed, 512 insertions(+), 69 deletions(-) create mode 100644 lib/src/importer/node_package.dart diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index daa2233db..a940d3f26 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -26,7 +26,8 @@ import 'visitor/serialize.dart'; /// Like [compileAsync] in `lib/sass.dart`, but provides more options to support /// the node-sass compatible API and the executable. /// -/// At most one of `importCache` and `nodeImporter` may be provided at once. +/// If both `importCache` and `nodeImporter` are provided, the importers in +/// `importCache` will be evaluated before `nodeImporter`. Future compileAsync(String path, {Syntax? syntax, Logger? logger, @@ -56,7 +57,7 @@ Future compileAsync(String path, (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= AsyncImportCache.none(logger: logger); stylesheet = (await importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), + FilesystemImporter.cwd, p.toUri(canonicalize(path)), originalUrl: p.toUri(path)))!; } else { stylesheet = Stylesheet.parse( @@ -69,7 +70,7 @@ Future compileAsync(String path, logger, importCache, nodeImporter, - FilesystemImporter('.'), + FilesystemImporter.cwd, functions, style, useSpaces, @@ -121,7 +122,7 @@ Future compileStringAsync(String source, logger, importCache, nodeImporter, - importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter('.')), + importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter.cwd), functions, style, useSpaces, diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index 9b08e5597..0deb6285f 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -104,6 +104,12 @@ final class AsyncImportCache { : _importers = const [], _logger = logger ?? const Logger.stderr(); + /// Creates an import cache without any globally-available importers, and only + /// the passed in importers. + AsyncImportCache.only(Iterable importers, {Logger? logger}) + : _importers = List.unmodifiable(importers), + _logger = logger ?? const Logger.stderr(); + /// Converts the user's [importers], [loadPaths], and [packageConfig] /// options into a single list of importers. static List _toImporters(Iterable? importers, diff --git a/lib/src/compile.dart b/lib/src/compile.dart index b951c8036..94221405d 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: 5178e366228bde7854df12221393857bb3022628 +// Checksum: a9421a2975e79ad591ae32474cd076e1379d0e75 // // ignore_for_file: unused_import @@ -35,7 +35,8 @@ import 'visitor/serialize.dart'; /// Like [compile] in `lib/sass.dart`, but provides more options to support /// the node-sass compatible API and the executable. /// -/// At most one of `importCache` and `nodeImporter` may be provided at once. +/// If both `importCache` and `nodeImporter` are provided, the importers in +/// `importCache` will be evaluated before `nodeImporter`. CompileResult compile(String path, {Syntax? syntax, Logger? logger, @@ -65,7 +66,7 @@ CompileResult compile(String path, (syntax == null || syntax == Syntax.forPath(path))) { importCache ??= ImportCache.none(logger: logger); stylesheet = importCache.importCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(path)), + FilesystemImporter.cwd, p.toUri(canonicalize(path)), originalUrl: p.toUri(path))!; } else { stylesheet = Stylesheet.parse( @@ -78,7 +79,7 @@ CompileResult compile(String path, logger, importCache, nodeImporter, - FilesystemImporter('.'), + FilesystemImporter.cwd, functions, style, useSpaces, @@ -130,7 +131,7 @@ CompileResult compileString(String source, logger, importCache, nodeImporter, - importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter('.')), + importer ?? (isBrowser ? NoOpImporter() : FilesystemImporter.cwd), functions, style, useSpaces, diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 4e390fcd9..356130d84 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -11,6 +11,7 @@ import 'package:native_synchronization/mailbox.dart'; import 'package:path/path.dart' as p; import 'package:protobuf/protobuf.dart'; import 'package:sass/sass.dart' as sass; +import 'package:sass/src/importer/node_package.dart' as npi; import '../logger.dart'; import '../value/function.dart'; @@ -226,6 +227,10 @@ final class CompilationDispatcher { case InboundMessage_CompileRequest_Importer_Importer.notSet: _checkNoNonCanonicalScheme(importer); return null; + + case InboundMessage_CompileRequest_Importer_Importer.nodePackageImporter: + return npi.NodePackageImporter( + importer.nodePackageImporter.entryPointDirectory); } } diff --git a/lib/src/embedded/importer/file.dart b/lib/src/embedded/importer/file.dart index d6096eb7b..57d97ddf9 100644 --- a/lib/src/embedded/importer/file.dart +++ b/lib/src/embedded/importer/file.dart @@ -6,12 +6,6 @@ import '../../importer.dart'; import '../embedded_sass.pb.dart' hide SourceSpan; import 'base.dart'; -/// A filesystem importer to use for most implementation details of -/// [FileImporter]. -/// -/// This allows us to avoid duplicating logic between the two importers. -final _filesystemImporter = FilesystemImporter('.'); - /// An importer that asks the host to resolve imports in a simplified, /// file-system-centric way. final class FileImporter extends ImporterBase { @@ -21,7 +15,7 @@ final class FileImporter extends ImporterBase { FileImporter(super.dispatcher, this._importerId); Uri? canonicalize(Uri url) { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); var request = OutboundMessage_FileImportRequest() ..importerId = _importerId @@ -39,7 +33,7 @@ final class FileImporter extends ImporterBase { throw 'The file importer must return a file: URL, was "$url"'; } - return _filesystemImporter.canonicalize(url); + return FilesystemImporter.cwd.canonicalize(url); case InboundMessage_FileImportResponse_Result.error: throw response.error; @@ -49,7 +43,7 @@ final class FileImporter extends ImporterBase { } } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); bool isNonCanonicalScheme(String scheme) => scheme != 'file'; diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 70b52ba10..ba85610af 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -68,7 +68,7 @@ Future<(int, String, String?)?> compileStylesheet(ExecutableOptions options, Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, StylesheetGraph graph, String? source, String? destination, {bool ifModified = false}) async { - var importer = FilesystemImporter('.'); + var importer = FilesystemImporter.cwd; if (ifModified) { try { if (source != null && @@ -102,7 +102,7 @@ Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, syntax: syntax, logger: options.logger, importCache: importCache, - importer: FilesystemImporter('.'), + importer: FilesystemImporter.cwd, style: options.style, quietDeps: options.quietDeps, verbose: options.verbose, @@ -127,7 +127,7 @@ Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, syntax: syntax, logger: options.logger, importCache: graph.importCache, - importer: FilesystemImporter('.'), + importer: FilesystemImporter.cwd, style: options.style, quietDeps: options.quietDeps, verbose: options.verbose, diff --git a/lib/src/executable/repl.dart b/lib/src/executable/repl.dart index d460b40e0..6e0124bde 100644 --- a/lib/src/executable/repl.dart +++ b/lib/src/executable/repl.dart @@ -22,7 +22,7 @@ Future repl(ExecutableOptions options) async { var repl = Repl(prompt: '>> '); var logger = TrackingLogger(options.logger); var evaluator = Evaluator( - importer: FilesystemImporter('.'), + importer: FilesystemImporter.cwd, importCache: ImportCache(loadPaths: options.loadPaths, logger: logger), logger: logger); await for (String line in repl.runAsync()) { diff --git a/lib/src/executable/watch.dart b/lib/src/executable/watch.dart index c8a222b0b..9e1db78e9 100644 --- a/lib/src/executable/watch.dart +++ b/lib/src/executable/watch.dart @@ -39,7 +39,7 @@ Future watch(ExecutableOptions options, StylesheetGraph graph) async { var sourcesToDestinations = _sourcesToDestinations(options); for (var source in sourcesToDestinations.keys) { graph.addCanonical( - FilesystemImporter('.'), p.toUri(canonicalize(source)), p.toUri(source), + FilesystemImporter.cwd, p.toUri(canonicalize(source)), p.toUri(source), recanonicalize: false); } var success = await compileStylesheets(options, graph, sourcesToDestinations, @@ -130,7 +130,7 @@ final class _Watcher { await compileStylesheets(_options, _graph, {path: destination}, ifModified: true); var downstream = _graph.addCanonical( - FilesystemImporter('.'), _canonicalize(path), p.toUri(path)); + FilesystemImporter.cwd, _canonicalize(path), p.toUri(path)); return await _recompileDownstream(downstream) && success; } @@ -144,7 +144,7 @@ final class _Watcher { if (_destinationFor(path) case var destination?) _delete(destination); } - var downstream = _graph.remove(FilesystemImporter('.'), url); + var downstream = _graph.remove(FilesystemImporter.cwd, url); return await _recompileDownstream(downstream); } diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index 397e676aa..e34f0a7ee 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: 342e907cf10e1dd80d7045fc32db43c74376654e +// Checksum: d157b83599dbc07a80ac6cb5ffdf5dde03b60376 // // ignore_for_file: unused_import @@ -106,6 +106,12 @@ final class ImportCache { : _importers = const [], _logger = logger ?? const Logger.stderr(); + /// Creates an import cache without any globally-available importers, and only + /// the passed in importers. + ImportCache.only(Iterable importers, {Logger? logger}) + : _importers = List.unmodifiable(importers), + _logger = logger ?? const Logger.stderr(); + /// Converts the user's [importers], [loadPaths], and [packageConfig] /// options into a single list of importers. static List _toImporters(Iterable? importers, diff --git a/lib/src/importer/filesystem.dart b/lib/src/importer/filesystem.dart index 31af69829..47b0ae288 100644 --- a/lib/src/importer/filesystem.dart +++ b/lib/src/importer/filesystem.dart @@ -22,6 +22,9 @@ class FilesystemImporter extends Importer { /// Creates an importer that loads files relative to [loadPath]. FilesystemImporter(String loadPath) : _loadPath = p.absolute(loadPath); + /// Creates an importer relative to the current working directory. + static final cwd = FilesystemImporter('.'); + Uri? canonicalize(Uri url) { if (url.scheme != 'file' && url.scheme != '') return null; return resolveImportPath(p.join(_loadPath, p.fromUri(url))) diff --git a/lib/src/importer/js_to_dart/async_file.dart b/lib/src/importer/js_to_dart/async_file.dart index e984531dc..7be4b9461 100644 --- a/lib/src/importer/js_to_dart/async_file.dart +++ b/lib/src/importer/js_to_dart/async_file.dart @@ -17,12 +17,6 @@ import '../filesystem.dart'; import '../result.dart'; import '../utils.dart'; -/// A filesystem importer to use for most implementation details of -/// [JSToDartAsyncFileImporter]. -/// -/// This allows us to avoid duplicating logic between the two importers. -final _filesystemImporter = FilesystemImporter('.'); - /// A wrapper for a potentially-asynchronous JS API file importer that exposes /// it as a Dart [AsyncImporter]. final class JSToDartAsyncFileImporter extends AsyncImporter { @@ -32,7 +26,7 @@ final class JSToDartAsyncFileImporter extends AsyncImporter { JSToDartAsyncFileImporter(this._findFileUrl); FutureOr canonicalize(Uri url) async { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); var result = wrapJSExceptions(() => _findFileUrl( url.toString(), @@ -52,16 +46,16 @@ final class JSToDartAsyncFileImporter extends AsyncImporter { '"$url".')); } - return _filesystemImporter.canonicalize(resultUrl); + return FilesystemImporter.cwd.canonicalize(resultUrl); } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); DateTime modificationTime(Uri url) => - _filesystemImporter.modificationTime(url); + FilesystemImporter.cwd.modificationTime(url); bool couldCanonicalize(Uri url, Uri canonicalUrl) => - _filesystemImporter.couldCanonicalize(url, canonicalUrl); + FilesystemImporter.cwd.couldCanonicalize(url, canonicalUrl); bool isNonCanonicalScheme(String scheme) => scheme != 'file'; } diff --git a/lib/src/importer/js_to_dart/file.dart b/lib/src/importer/js_to_dart/file.dart index 9ad474d00..e3302f881 100644 --- a/lib/src/importer/js_to_dart/file.dart +++ b/lib/src/importer/js_to_dart/file.dart @@ -12,12 +12,6 @@ import '../../js/utils.dart'; import '../../util/nullable.dart'; import '../utils.dart'; -/// A filesystem importer to use for most implementation details of -/// [JSToDartAsyncFileImporter]. -/// -/// This allows us to avoid duplicating logic between the two importers. -final _filesystemImporter = FilesystemImporter('.'); - /// A wrapper for a potentially-asynchronous JS API file importer that exposes /// it as a Dart [AsyncImporter]. final class JSToDartFileImporter extends Importer { @@ -27,7 +21,7 @@ final class JSToDartFileImporter extends Importer { JSToDartFileImporter(this._findFileUrl); Uri? canonicalize(Uri url) { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); var result = wrapJSExceptions(() => _findFileUrl( url.toString(), @@ -51,16 +45,16 @@ final class JSToDartFileImporter extends Importer { '"$url".')); } - return _filesystemImporter.canonicalize(resultUrl); + return FilesystemImporter.cwd.canonicalize(resultUrl); } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); DateTime modificationTime(Uri url) => - _filesystemImporter.modificationTime(url); + FilesystemImporter.cwd.modificationTime(url); bool couldCanonicalize(Uri url, Uri canonicalUrl) => - _filesystemImporter.couldCanonicalize(url, canonicalUrl); + FilesystemImporter.cwd.couldCanonicalize(url, canonicalUrl); bool isNonCanonicalScheme(String scheme) => scheme != 'file'; } diff --git a/lib/src/importer/node_package.dart b/lib/src/importer/node_package.dart new file mode 100644 index 000000000..ff7b51ca7 --- /dev/null +++ b/lib/src/importer/node_package.dart @@ -0,0 +1,387 @@ +// 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 'package:cli_pkg/js.dart'; +import 'package:collection/collection.dart'; +import 'package:sass/src/util/map.dart'; +import 'package:sass/src/util/nullable.dart'; + +import '../importer.dart'; +import './utils.dart'; +import 'dart:convert'; +import '../io.dart'; +import 'package:path/path.dart' as p; + +/// An [Importer] that resolves `pkg:` URLs using the Node resolution algorithm. +class NodePackageImporter extends Importer { + /// The starting path for canonicalizations without a containing URL. + late final String _entryPointDirectory; + + /// Creates a Node package importer with the associated entry point. + NodePackageImporter(String? entryPointDirectory) { + if (entryPointDirectory == null) { + throw "The Node package importer cannot determine an entry point " + "because `require.main.filename` is not defined. " + "Please provide an `entryPointDirectory` to the `NodePackageImporter`."; + } else if (isBrowser) { + throw "The Node package importer cannot be used without a filesystem."; + } + _entryPointDirectory = p.absolute(entryPointDirectory); + } + + @override + bool isNonCanonicalScheme(String scheme) => scheme == 'pkg'; + + @override + Uri? canonicalize(Uri url) { + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); + if (url.scheme != 'pkg') return null; + + if (url.hasAuthority) { + throw "A pkg: URL must not have a host, port, username or password."; + } else if (p.url.isAbsolute(url.path)) { + throw "A pkg: URL's path must not begin with /."; + } else if (url.path.isEmpty) { + throw "A pkg: URL must not have an empty path."; + } else if (url.hasQuery || url.hasFragment) { + throw "A pkg: URL must not have a query or fragment."; + } + + var baseDirectory = containingUrl?.scheme == 'file' + ? p.dirname(p.fromUri(containingUrl!)) + : _entryPointDirectory; + + var (packageName, subpath) = _packageNameAndSubpath(url.path); + + // If the package name is not a valid Node package name, return null in case + // another importer can handle. + if (packageName.startsWith('.') || + packageName.contains('\\') || + packageName.contains('%') || + (packageName.startsWith('@') && + !packageName.contains(p.url.separator))) { + return null; + } + + var packageRoot = _resolvePackageRoot(packageName, baseDirectory); + + if (packageRoot == null) return null; + var jsonPath = p.join(packageRoot, 'package.json'); + + var jsonString = readFile(jsonPath); + Map packageManifest; + try { + packageManifest = json.decode(jsonString) as Map; + } catch (e) { + throw "Failed to parse $jsonPath for \"pkg:$packageName\": $e"; + } + + if (_resolvePackageExports( + packageRoot, subpath, packageManifest, packageName) + case var resolved?) { + if (_validExtensions.contains(p.extension(resolved))) { + return p.toUri(p.canonicalize(resolved)); + } else { + throw "The export for '${subpath ?? "root"}' in " + "'$packageName' resolved to '${resolved.toString()}', " + "which is not a '.scss', '.sass', or '.css' file."; + } + } + // If no subpath, attempt to resolve `sass` or `style` key in package.json, + // then `index` file at package root, resolved for file extensions and + // partials. + if (subpath == null) { + var rootPath = _resolvePackageRootValues(packageRoot, packageManifest); + return rootPath != null ? p.toUri(p.canonicalize(rootPath)) : null; + } + + // If there is a subpath, attempt to resolve the path relative to the + // package root, and resolve for file extensions and partials. + var subpathInRoot = p.join(packageRoot, subpath); + return FilesystemImporter.cwd.canonicalize(p.toUri(subpathInRoot)); + } + + @override + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); + + /// Splits a [bare import + /// specifier](https://nodejs.org/api/esm.html#import-specifiers) `specifier` + /// into its package name and subpath, if one exists. + /// + /// Because this is a bare import specifier and not a path, we always use `/` + /// to avoid invalid values on non-Posix machines. + (String, String?) _packageNameAndSubpath(String specifier) { + var parts = p.url.split(specifier); + var name = p.fromUri(parts.removeAt(0)); + + if (name.startsWith('@')) { + if (parts.isNotEmpty) name = p.url.join(name, parts.removeAt(0)); + } + var subpath = parts.isNotEmpty ? p.fromUri(p.url.joinAll(parts)) : null; + return (name, subpath); + } + + /// Returns an absolute path to the root directory for the most proximate + /// installed `packageName`. + /// + /// Implementation of `PACKAGE_RESOLVE` from the [Resolution Algorithm + /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification). + String? _resolvePackageRoot(String packageName, String baseDirectory) { + while (true) { + var potentialPackage = p.join(baseDirectory, 'node_modules', packageName); + if (dirExists(potentialPackage)) return potentialPackage; + // baseDirectory has now reached root without finding a match. + if (p.split(baseDirectory).length == 1) return null; + baseDirectory = p.dirname(baseDirectory); + } + } + + /// Returns a file path specified by the `sass` or `style` values in a package + /// manifest, or an `index` file relative to the package root. + String? _resolvePackageRootValues( + String packageRoot, Map packageManifest) { + if (packageManifest['sass'] case String sassValue + when _validExtensions.contains(p.url.extension(sassValue))) { + return p.join(packageRoot, sassValue); + } else if (packageManifest['style'] case String styleValue + when _validExtensions.contains(p.url.extension(styleValue))) { + return p.join(packageRoot, styleValue); + } + + var result = resolveImportPath(p.join(packageRoot, 'index')); + return result; + } + + /// Returns a file path specified by a `subpath` in the `exports` section of + /// package.json. + /// + /// `packageName` is used for error reporting. + String? _resolvePackageExports(String packageRoot, String? subpath, + Map packageManifest, String packageName) { + var exports = packageManifest['exports'] as Object?; + if (exports == null) return null; + var subpathVariants = _exportsToCheck(subpath); + if (_nodePackageExportsResolve( + packageRoot, subpathVariants, exports, subpath, packageName) + case var path?) { + return path; + } + + if (subpath != null && p.url.extension(subpath).isNotEmpty) return null; + + var subpathIndexVariants = _exportsToCheck(subpath, addIndex: true); + if (_nodePackageExportsResolve( + packageRoot, subpathIndexVariants, exports, subpath, packageName) + case var path?) { + return path; + } + + return null; + } + + /// Returns the path to one subpath variant, resolved in the `exports` of a + /// package manifest. + /// + /// Throws an error if multiple `subpathVariants` match, and null if none + /// match. + /// + /// Implementation of `PACKAGE_EXPORTS_RESOLVE` from the [Resolution Algorithm + /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification). + String? _nodePackageExportsResolve( + String packageRoot, + List subpathVariants, + Object exports, + String? subpath, + String packageName) { + if (exports is Map && + exports.keys.any((key) => key.startsWith('.')) && + exports.keys.any((key) => !key.startsWith('.'))) { + throw '`exports` in $packageName can not have both conditions and paths ' + 'at the same level.\n' + 'Found ${exports.keys.map((key) => '"$key"').join(',')} in ' + '${p.join(packageRoot, 'package.json')}.'; + } + + var matches = subpathVariants + .map((String? variant) { + if (variant == null) { + return _getMainExport(exports).andThen((mainExport) => + _packageTargetResolve(variant, mainExport, packageRoot)); + } else if (exports is! Map || + exports.keys.every((key) => !key.startsWith('.'))) { + return null; + } + var matchKey = "./${p.toUri(variant)}"; + if (exports.containsKey(matchKey) && + exports[matchKey] != null && + !matchKey.contains('*')) { + return _packageTargetResolve( + matchKey, exports[matchKey] as Object, packageRoot); + } + + var expansionKeys = [ + for (var key in exports.keys) + if ('*'.allMatches(key).length == 1) key + ]..sort(_compareExpansionKeys); + + for (var expansionKey in expansionKeys) { + var [patternBase, patternTrailer] = expansionKey.split('*'); + if (!matchKey.startsWith(patternBase)) continue; + if (matchKey == patternBase) continue; + if (patternTrailer.isEmpty || + (matchKey.endsWith(patternTrailer) && + matchKey.length >= expansionKey.length)) { + var target = exports[expansionKey] as Object?; + if (target == null) continue; + var patternMatch = matchKey.substring( + patternBase.length, matchKey.length - patternTrailer.length); + return _packageTargetResolve( + variant, target, packageRoot, patternMatch); + } + } + + return null; + }) + .whereNotNull() + .toList(); + + return switch (matches) { + [var path] => path, + [] => null, + var paths => + throw "Unable to determine which of multiple potential resolutions " + "found for ${subpath ?? 'root'} in $packageName should be used. " + "\n\nFound:\n" + "${paths.join('\n')}" + }; + } + + /// Implementation of the `PATTERN_KEY_COMPARE` comparator from + /// https://nodejs.org/api/esm.html#resolution-algorithm-specification. + int _compareExpansionKeys(String keyA, String keyB) { + var baseLengthA = keyA.contains('*') ? keyA.indexOf('*') + 1 : keyA.length; + var baseLengthB = keyB.contains('*') ? keyB.indexOf('*') + 1 : keyB.length; + if (baseLengthA > baseLengthB) return -1; + if (baseLengthB > baseLengthA) return 1; + if (!keyA.contains("*")) return 1; + if (!keyB.contains("*")) return -1; + if (keyA.length > keyB.length) return -1; + if (keyB.length > keyA.length) return 1; + return 0; + } + + /// Returns a file path for `subpath`, as resolved in the `exports` object. + /// + /// Verifies the file exists relative to `packageRoot`. Instances of `*` will + /// be replaced with `patternMatch`. + /// + /// `subpath` and `packageRoot` are native paths, and `patternMatch` is a URL + /// path. + /// + /// Implementation of `PACKAGE_TARGET_RESOLVE` from the [Resolution Algorithm + /// Specification](https://nodejs.org/api/esm.html#resolution-algorithm-specification). + String? _packageTargetResolve( + String? subpath, Object exports, String packageRoot, + [String? patternMatch]) { + switch (exports) { + case String string when !string.startsWith('./'): + throw "Export '$string' must be a path relative to the package root at '$packageRoot'."; + case String string when patternMatch != null: + var replaced = p.fromUri(string.replaceFirst('*', patternMatch)); + var path = p.normalize(p.join(packageRoot, replaced)); + return fileExists(path) ? path : null; + case String string: + return p.join(packageRoot, p.fromUri(string)); + case Map map: + for (var (key, value) in map.pairs) { + if (!const {'sass', 'style', 'default'}.contains(key)) continue; + if (value == null) continue; + if (_packageTargetResolve( + subpath, value as Object, packageRoot, patternMatch) + case var result?) { + return result; + } + } + return null; + + case []: + return null; + + case List array: + for (var value in array) { + if (value == null) continue; + if (_packageTargetResolve( + subpath, value as Object, packageRoot, patternMatch) + case var result?) { + return result; + } + } + + return null; + + default: + throw "Invalid 'exports' value $exports in " + "${p.join(packageRoot, 'package.json')}."; + } + } + + /// Returns a path to a package's export without a subpath. + Object? _getMainExport(Object exports) { + return switch (exports) { + String string => string, + List list => list, + Map map + when !map.keys.any((key) => key.startsWith('.')) => + map, + {'.': var export?} => export, + _ => null + }; + } + + /// Returns a list of all possible variations of `subpath` with extensions and + /// partials. + /// + /// If there is no subpath, returns a single `null` value, which is used in + /// `_nodePackageExportsResolve` to denote the main package export. + List _exportsToCheck(String? subpath, {bool addIndex = false}) { + var paths = []; + + if (subpath == null && addIndex) { + subpath = 'index'; + } else if (subpath != null && addIndex) { + subpath = p.join(subpath, 'index'); + } + if (subpath == null) return [null]; + + if (_validExtensions.contains(p.url.extension(subpath))) { + paths.add(subpath); + } else { + paths.addAll([ + '$subpath.scss', + '$subpath.sass', + '$subpath.css', + ]); + } + var basename = p.basename(subpath); + var dirname = p.dirname(subpath); + + if (basename.startsWith('_')) return paths; + + return [ + ...paths, + for (var path in paths) + if (dirname == '.') + '_${p.basename(path)}' + else + p.join(dirname, '_${p.basename(path)}') + ]; + } +} + +/// The set of file extensions that Sass can parse. +/// +/// `NodePackageImporter` will only resolve files with these extensions, and +/// uses these extensions to check for matches if no extension is provided in +/// the Url to canonicalize. +const _validExtensions = {'.scss', '.sass', '.css'}; diff --git a/lib/src/importer/package.dart b/lib/src/importer/package.dart index 21f41509f..39d09ac63 100644 --- a/lib/src/importer/package.dart +++ b/lib/src/importer/package.dart @@ -7,12 +7,6 @@ import 'package:package_config/package_config_types.dart'; import '../importer.dart'; -/// A filesystem importer to use when resolving the results of `package:` URLs. -/// -/// This allows us to avoid duplicating the logic for choosing an extension and -/// looking for partials. -final _filesystemImporter = FilesystemImporter('.'); - /// An importer that loads stylesheets from `package:` imports. /// /// {@category Importer} @@ -29,7 +23,7 @@ class PackageImporter extends Importer { PackageImporter(PackageConfig packageConfig) : _packageConfig = packageConfig; Uri? canonicalize(Uri url) { - if (url.scheme == 'file') return _filesystemImporter.canonicalize(url); + if (url.scheme == 'file') return FilesystemImporter.cwd.canonicalize(url); if (url.scheme != 'package') return null; var resolved = _packageConfig.resolve(url); @@ -39,17 +33,18 @@ class PackageImporter extends Importer { throw "Unsupported URL $resolved."; } - return _filesystemImporter.canonicalize(resolved); + return FilesystemImporter.cwd.canonicalize(resolved); } - ImporterResult? load(Uri url) => _filesystemImporter.load(url); + ImporterResult? load(Uri url) => FilesystemImporter.cwd.load(url); DateTime modificationTime(Uri url) => - _filesystemImporter.modificationTime(url); + FilesystemImporter.cwd.modificationTime(url); bool couldCanonicalize(Uri url, Uri canonicalUrl) => (url.scheme == 'file' || url.scheme == 'package' || url.scheme == '') && - _filesystemImporter.couldCanonicalize(Uri(path: url.path), canonicalUrl); + FilesystemImporter.cwd + .couldCanonicalize(Uri(path: url.path), canonicalUrl); String toString() => "package:..."; } diff --git a/lib/src/js.dart b/lib/src/js.dart index 92ab23f66..3246fc743 100644 --- a/lib/src/js.dart +++ b/lib/src/js.dart @@ -51,6 +51,7 @@ void main() { silent: JSLogger( warn: allowInteropNamed('sass.Logger.silent.warn', (_, __) {}), debug: allowInteropNamed('sass.Logger.silent.debug', (_, __) {}))); + exports.NodePackageImporter = nodePackageImporterClass; 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 b0a192a9e..0a2c0c9b0 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -6,6 +6,7 @@ import 'package:cli_pkg/js.dart'; 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 '../../sass.dart'; import '../importer/no_op.dart'; @@ -13,6 +14,7 @@ import '../importer/js_to_dart/async.dart'; import '../importer/js_to_dart/async_file.dart'; import '../importer/js_to_dart/file.dart'; import '../importer/js_to_dart/sync.dart'; +import '../importer/node_package.dart'; import '../io.dart'; import '../logger/js_to_dart.dart'; import '../util/nullable.dart'; @@ -20,6 +22,7 @@ import 'compile_options.dart'; import 'compile_result.dart'; import 'exception.dart'; import 'importer.dart'; +import 'reflection.dart'; import 'utils.dart'; /// The JS API `compile` function. @@ -182,6 +185,8 @@ OutputStyle _parseOutputStyle(String? style) => switch (style) { /// Converts [importer] into an [AsyncImporter] that can be used with /// [compileAsync] or [compileStringAsync]. AsyncImporter _parseAsyncImporter(Object? importer) { + if (importer is NodePackageImporter) return importer; + if (importer == null) jsThrow(JsError("Importers may not be null.")); importer as JSImporter; @@ -207,6 +212,8 @@ AsyncImporter _parseAsyncImporter(Object? importer) { /// Converts [importer] into a synchronous [Importer]. Importer _parseImporter(Object? importer) { + if (importer is NodePackageImporter) return importer; + if (importer == null) jsThrow(JsError("Importers may not be null.")); importer as JSImporter; @@ -321,3 +328,16 @@ List _parseFunctions(Object? functions, {bool asynch = false}) { }); return result; } + +/// The exported `NodePackageImporter` class that can be added to the +/// `importers` option to enable loading `pkg:` URLs from `node_modules`. +final JSClass nodePackageImporterClass = () { + var jsClass = createJSClass( + 'sass.NodePackageImporter', + (Object self, [String? entryPointDirectory]) => NodePackageImporter( + entryPointDirectory ?? + (requireMainFilename != null + ? p.dirname(requireMainFilename!) + : null))); + return jsClass; +}(); diff --git a/lib/src/js/exports.dart b/lib/src/js/exports.dart index 3cf5bb7a5..9a45268a8 100644 --- a/lib/src/js/exports.dart +++ b/lib/src/js/exports.dart @@ -26,6 +26,7 @@ class Exports { external set info(String info); external set Exception(JSClass function); external set Logger(LoggerNamespace namespace); + external set NodePackageImporter(JSClass function); // Value APIs external set Value(JSClass function); diff --git a/lib/src/js/legacy.dart b/lib/src/js/legacy.dart index 5c5ad533a..ed4ba7584 100644 --- a/lib/src/js/legacy.dart +++ b/lib/src/js/legacy.dart @@ -10,6 +10,9 @@ import 'dart:typed_data'; import 'package:cli_pkg/js.dart'; import 'package:node_interop/js.dart'; import 'package:path/path.dart' as p; +import '../async_import_cache.dart'; +import '../import_cache.dart'; +import '../importer/node_package.dart'; import '../callable.dart'; import '../compile.dart'; @@ -76,6 +79,7 @@ Future _renderAsync(RenderOptions options) async { if (options.data case var data?) { result = await compileStringAsync(data, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImportersAsync(options, start), functions: _parseFunctions(options, start, asynch: true), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -92,6 +96,7 @@ Future _renderAsync(RenderOptions options) async { } else if (file != null) { result = await compileAsync(file, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImportersAsync(options, start), functions: _parseFunctions(options, start, asynch: true), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -129,6 +134,7 @@ RenderResult renderSync(RenderOptions options) { if (options.data case var data?) { result = compileString(data, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImporters(options, start), functions: _parseFunctions(options, start).cast(), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -145,6 +151,7 @@ RenderResult renderSync(RenderOptions options) { } else if (file != null) { result = compile(file, nodeImporter: _parseImporter(options, start), + importCache: _parsePackageImporters(options, start), functions: _parseFunctions(options, start).cast(), syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, style: _parseOutputStyle(options.outputStyle), @@ -289,6 +296,23 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) { return NodeImporter(contextOptions, includePaths, importers); } +/// Creates an [AsyncImportCache] for Package Importers. +AsyncImportCache? _parsePackageImportersAsync( + RenderOptions options, DateTime start) { + if (options.pkgImporter is NodePackageImporter) { + return AsyncImportCache.only([options.pkgImporter!]); + } + return null; +} + +/// Creates an [ImportCache] for Package Importers. +ImportCache? _parsePackageImporters(RenderOptions options, DateTime start) { + if (options.pkgImporter is NodePackageImporter) { + return ImportCache.only([options.pkgImporter!]); + } + return null; +} + /// Creates the [RenderContextOptions] for the `this` context in which custom /// functions and importers will be evaluated. RenderContextOptions _contextOptions(RenderOptions options, DateTime start) { diff --git a/lib/src/js/legacy/render_options.dart b/lib/src/js/legacy/render_options.dart index 3357166de..ac8cc61b8 100644 --- a/lib/src/js/legacy/render_options.dart +++ b/lib/src/js/legacy/render_options.dart @@ -4,6 +4,7 @@ import 'package:js/js.dart'; +import '../../importer/node_package.dart'; import '../logger.dart'; import 'fiber.dart'; @@ -13,6 +14,7 @@ class RenderOptions { external String? get file; external String? get data; external Object? get importer; + external NodePackageImporter? get pkgImporter; external Object? get functions; external List? get includePaths; external bool? get indentedSyntax; @@ -36,6 +38,7 @@ class RenderOptions { {String? file, String? data, Object? importer, + NodePackageImporter? pkgImporter, Object? functions, List? includePaths, bool? indentedSyntax, diff --git a/lib/src/js/utils.dart b/lib/src/js/utils.dart index 687484c9a..08fdd8f6b 100644 --- a/lib/src/js/utils.dart +++ b/lib/src/js/utils.dart @@ -233,3 +233,7 @@ Syntax parseSyntax(String? syntax) => switch (syntax) { 'css' => Syntax.css, _ => jsThrow(JsError('Unknown syntax "$syntax".')) }; + +/// The value of require.main.filename +@JS("require.main.filename") +external String? get requireMainFilename; diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 7d75430f7..26679cd8e 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -337,9 +337,10 @@ final class _EvaluateVisitor Logger? logger, bool quietDeps = false, bool sourceMap = false}) - : _importCache = nodeImporter == null - ? importCache ?? AsyncImportCache.none(logger: logger) - : null, + : _importCache = importCache ?? + (nodeImporter == null + ? AsyncImportCache.none(logger: logger) + : null), _nodeImporter = nodeImporter, _logger = logger ?? const Logger.stderr(), _quietDeps = quietDeps, @@ -1706,7 +1707,9 @@ final class _EvaluateVisitor return (stylesheet, importer: importer, isDependency: isDependency); } } - } else { + } + + if (_nodeImporter != null) { if (await _importLikeNode( url, baseUrl ?? _stylesheet.span.sourceUrl, forImport) case var result?) { diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 37c2e0c08..096ae21fe 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: 71dcf1747eb45036d3c1b5b57bd0cd5dbe6b8e14 +// Checksum: 7351193aa9229e1434c09a2cbc9fa596cd924901 // // ignore_for_file: unused_import @@ -345,9 +345,8 @@ final class _EvaluateVisitor Logger? logger, bool quietDeps = false, bool sourceMap = false}) - : _importCache = nodeImporter == null - ? importCache ?? ImportCache.none(logger: logger) - : null, + : _importCache = importCache ?? + (nodeImporter == null ? ImportCache.none(logger: logger) : null), _nodeImporter = nodeImporter, _logger = logger ?? const Logger.stderr(), _quietDeps = quietDeps, @@ -1702,7 +1701,9 @@ final class _EvaluateVisitor return (stylesheet, importer: importer, isDependency: isDependency); } } - } else { + } + + if (_nodeImporter != null) { if (_importLikeNode( url, baseUrl ?? _stylesheet.span.sourceUrl, forImport) case var result?) { diff --git a/pubspec.yaml b/pubspec.yaml index f14d6b822..eb90882c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: args: ^2.0.0 async: ^2.5.0 charcode: ^1.2.0 - cli_pkg: ^2.7.0 + cli_pkg: ^2.7.1 cli_repl: ^0.2.1 collection: ^1.16.0 http: "^1.1.0" From 84f31f0defcdcda648e3e11fb345ab4da1588062 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 6 Feb 2024 16:04:14 -0800 Subject: [PATCH 17/35] Update pubspec/changelog for `pkg:` importers (#2168) --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ pubspec.yaml | 2 +- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7374727f9..5a033cb43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +## 1.71.0 + +For more information about `pkg:` importers, see [the +announcement][pkg-importers] on the Sass blog. + +[pkg-importers]: https://sass-lang.com/blog/announcing-pkg-importers + +### JavaScript API + +* Add a `NodePackageImporter` importer that can be passed to the `importers` + option. This loads files using the `pkg:` URL scheme according to the Node.js + package resolution algorithm. For example, `@use "pkg:bootstrap"` will load + `node_modules/bootstrap/scss/bootstrap.scss`. The constructor takes a single + optional argument, which indicates the base directory to use when locating + `node_modules` directories. It defaults to + `path.dirname(require.main.filename)`. + +### Dart API + +* Add a `NodePackageImporter` importer that can be passed to the `importers` + option. This loads files using the `pkg:` URL scheme according to the Node.js + package resolution algorithm. For example, `@use "pkg:bootstrap"` will load + `node_modules/bootstrap/scss/bootstrap.scss`. The constructor takes a single + argument, which indicates the base directory to use when locating + `node_modules` directories. + ## 1.70.0 ### JavaScript API diff --git a/pubspec.yaml b/pubspec.yaml index eb90882c3..296270dfc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.70.0 +version: 1.71.0-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From 00571ec531220235a9ca90357670f0b000955a5f Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Mon, 12 Feb 2024 15:57:49 -0800 Subject: [PATCH 18/35] Add a `--pkg-importer` flag (#2169) See sass/sass#2739 --- CHANGELOG.md | 7 +++++++ bin/sass.dart | 1 + lib/src/executable/compile_stylesheet.dart | 4 +++- lib/src/executable/options.dart | 13 +++++++++++++ lib/src/executable/repl.dart | 5 ++++- pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 8 files changed, 35 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a033cb43..bb67ee905 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ announcement][pkg-importers] on the Sass blog. [pkg-importers]: https://sass-lang.com/blog/announcing-pkg-importers +### Command-Line Interface + +* Add a `--pkg-importer` flag to enable built-in `pkg:` importers. Currently + this only supports the Node.js package resolution algorithm, via + `--pkg-importer=node`. For example, `@use "pkg:bootstrap"` will load + `node_modules/bootstrap/scss/bootstrap.scss`. + ### JavaScript API * Add a `NodePackageImporter` importer that can be passed to the `importers` diff --git a/bin/sass.dart b/bin/sass.dart index ad23649d4..67b1782ed 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -45,6 +45,7 @@ Future main(List args) async { } var graph = StylesheetGraph(ImportCache( + importers: options.pkgImporters, loadPaths: options.loadPaths, // This logger is only used for handling fatal/future deprecations // during parsing, and is re-used across parses, so we don't want to diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index ba85610af..160a01d9a 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -95,7 +95,9 @@ Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, try { if (options.asynchronous) { var importCache = AsyncImportCache( - loadPaths: options.loadPaths, logger: options.logger); + importers: options.pkgImporters, + loadPaths: options.loadPaths, + logger: options.logger); result = source == null ? await compileStringAsync(await readStdin(), diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 504999c49..876b55814 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -10,6 +10,7 @@ import 'package:pub_semver/pub_semver.dart'; import 'package:term_glyph/term_glyph.dart' as term_glyph; import '../../sass.dart'; +import '../importer/node_package.dart'; import '../io.dart'; import '../util/character.dart'; @@ -47,6 +48,12 @@ final class ExecutableOptions { help: 'A path to use when resolving imports.\n' 'May be passed multiple times.', splitCommas: false) + ..addMultiOption('pkg-importer', + abbr: 'p', + valueHelp: 'TYPE', + allowed: ['node'], + help: 'Built-in importer(s) to use for pkg: URLs.', + allowedHelp: {'node': 'Load files like Node.js package resolution.'}) ..addOption('style', abbr: 's', valueHelp: 'NAME', @@ -218,6 +225,12 @@ final class ExecutableOptions { /// The set of paths Sass in which should look for imported files. List get loadPaths => _options['load-path'] as List; + /// The list of built-in importers to use to load `pkg:` URLs. + List get pkgImporters => [ + for (var _ in _options['pkg-importer'] as List) + NodePackageImporter('.') + ]; + /// Whether to run the evaluator in asynchronous mode, for debugging purposes. bool get asynchronous => _options['async'] as bool; diff --git a/lib/src/executable/repl.dart b/lib/src/executable/repl.dart index 6e0124bde..e2e858a26 100644 --- a/lib/src/executable/repl.dart +++ b/lib/src/executable/repl.dart @@ -23,7 +23,10 @@ Future repl(ExecutableOptions options) async { var logger = TrackingLogger(options.logger); var evaluator = Evaluator( importer: FilesystemImporter.cwd, - importCache: ImportCache(loadPaths: options.loadPaths, logger: logger), + importCache: ImportCache( + importers: options.pkgImporters, + loadPaths: options.loadPaths, + logger: logger), logger: logger); await for (String line in repl.runAsync()) { if (line.trim().isEmpty) continue; diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 99a7689e4..a0a629ba1 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.4.0 + +* No user-visible changes. + ## 9.3.0 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 5158fc4e4..ce05d56df 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: 9.3.0 +version: 9.4.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.70.0 + sass: 1.71.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 296270dfc..13edc951c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.71.0-dev +version: 1.71.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From 84ededd3688508acbbbf04c343f44734a8631a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 13 Feb 2024 15:10:50 -0800 Subject: [PATCH 19/35] Use musl support in cli_pkg (#2172) --- .github/workflows/build-linux-musl.yml | 31 +++++--------------------- pubspec.yaml | 2 +- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-linux-musl.yml b/.github/workflows/build-linux-musl.yml index 050292f4a..5f92b71a2 100644 --- a/.github/workflows/build-linux-musl.yml +++ b/.github/workflows/build-linux-musl.yml @@ -20,8 +20,12 @@ jobs: platform: linux/386 - arch: arm64 platform: linux/arm64 + # There is a bug in qemu's mremap causing pthread_getattr_np in musl to stuck in a loop on arm. + # Unless qemu fixes the bug or we get a real linux-arm runner, we cannot build aot-snapshot + # for arm on CI. So, we create a kernel snapshot for arm build in amd64 container instead. + # https://gitlab.com/qemu-project/qemu/-/issues/1729 - arch: arm - platform: linux/arm/v7 + platform: linux/amd64 # linux/arm/v7 steps: - uses: actions/checkout@v4 @@ -41,11 +45,6 @@ jobs: dart run grinder protobuf EOF - # https://gitlab.com/qemu-project/qemu/-/issues/1729 - # - # There is a bug in qemu's mremap causing pthread_getattr_np in musl to stuck in a loop on arm. - # Unless qemu fixes the bug or we get a real linux-arm runner, we cannot build aot-snapshot - # for arm on CI. So, we create a kernel snapshot for arm build in amd64 container instead. - name: Build run: | docker run --rm -i \ @@ -60,26 +59,6 @@ jobs: find build -name '*.tar.gz' -print0 | xargs -0 -n 1 -- sh -xc 'mv "$1" "$(echo "$1" | sed -e "s/linux/linux-musl/")"' -- EOF - # The kernel snapshot created for arm in the previous step is bundling a glibc based dart runtime - # due to how cli_pkg downloads the sdk for building non-native platforms. Therefore we need to - # replace it with musl-libc based dart runtime to create a working linux-musl-arm package. - - name: Fix Dart Runtime - if: matrix.arch == 'arm' - run: | - docker run --rm -i \ - --platform ${{ matrix.platform }} \ - --volume $PWD:$PWD \ - --workdir $PWD \ - ghcr.io/dart-musl/dart <<'EOF' - set -e - apk add --no-cache tar - cd build - DART_RUNTIME=$(tar -tzf *.tar.gz --wildcards "*/src/dart") - tar -xzf *.tar.gz - cp $DART_SDK/bin/dart $DART_RUNTIME - tar -czf *.tar.gz "$(dirname "$(dirname "$DART_RUNTIME")")" - EOF - - name: Upload Artifact uses: actions/upload-artifact@v4 with: diff --git a/pubspec.yaml b/pubspec.yaml index 13edc951c..c3a1596b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,7 +14,7 @@ dependencies: args: ^2.0.0 async: ^2.5.0 charcode: ^1.2.0 - cli_pkg: ^2.7.1 + cli_pkg: ^2.8.0 cli_repl: ^0.2.1 collection: ^1.16.0 http: "^1.1.0" From 2cab33e2b38771b9e83b5463aff7e73fa190fea6 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 15 Feb 2024 15:09:00 -0800 Subject: [PATCH 20/35] Update the language revision in Homebrew on release (#2171) --- tool/grind.dart | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tool/grind.dart b/tool/grind.dart index 92f919deb..fd6792926 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -29,6 +29,7 @@ void main(List args) { pkg.chocolateyNuspec.value = _nuspec; pkg.homebrewRepo.value = "sass/homebrew-sass"; pkg.homebrewFormula.value = "Formula/sass.rb"; + pkg.homebrewEditFormula.value = _updateHomebrewLanguageRevision; pkg.jsRequires.value = [ pkg.JSRequire("immutable", target: pkg.JSRequireTarget.all), pkg.JSRequire("chokidar", target: pkg.JSRequireTarget.cli), @@ -292,3 +293,30 @@ function defaultExportDeprecation() { File("build/npm/sass.node.mjs").writeAsStringSync(buffer.toString()); } + +/// A regular expression to locate the language repo revision in the Dart Sass +/// Homebrew formula. +final _homebrewLanguageRegExp = RegExp( + r'resource "language" do$' + r'(?:(?! end$).)+' + r'revision: "([a-f0-9]{40})"', + dotAll: true, + multiLine: true); + +/// Updates the Homebrew [formula] to change the revision of the language repo +/// to the latest revision. +String _updateHomebrewLanguageRevision(String formula) { + var languageRepoRevision = run("git", + arguments: ["ls-remote", "https://github.com/sass/sass"], quiet: true) + .split("\t") + .first; + + var match = _homebrewLanguageRegExp.firstMatch(formula); + if (match == null) { + fail("Couldn't find a language repo revision in the Homebrew formula."); + } + + return formula.substring(0, match.start) + + match.group(0)!.replaceFirst(match.group(1)!, languageRepoRevision) + + formula.substring(match.end); +} From 3e6721e79f049dd01880542667380640c2d1eeae Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Thu, 15 Feb 2024 17:29:34 -0800 Subject: [PATCH 21/35] Fix new static warnings with Dart 3.3 (#2173) --- lib/src/js/compile.dart | 2 +- lib/src/util/box.dart | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index 0a2c0c9b0..086123846 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -250,7 +250,7 @@ List? _normalizeNonCanonicalSchemes(Object? schemes) => }; /// Implements the simplification algorithm for custom function return `Value`s. -/// {@link https://github.com/sass/sass/blob/main/spec/types/calculation.md#simplifying-a-calculationvalue} +/// See https://github.com/sass/sass/blob/main/spec/types/calculation.md#simplifying-a-calculationvalue Value _simplifyValue(Value value) => switch (value) { SassCalculation() => switch (( // Match against... diff --git a/lib/src/util/box.dart b/lib/src/util/box.dart index cfd076669..50a9eb750 100644 --- a/lib/src/util/box.dart +++ b/lib/src/util/box.dart @@ -13,7 +13,7 @@ class Box { Box._(this._inner); - bool operator ==(Object? other) => other is Box && other._inner == _inner; + bool operator ==(Object other) => other is Box && other._inner == _inner; int get hashCode => _inner.hashCode; } From 786dd63ea1ba7f9bdfe1797c9741f3b6c99c8887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 20 Feb 2024 14:35:13 -0800 Subject: [PATCH 22/35] Fix linux musl builds (#2175) Co-authored-by: Natalie Weizenbaum --- .github/workflows/build-linux-musl.yml | 4 +--- CHANGELOG.md | 11 +++++++++++ pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-linux-musl.yml b/.github/workflows/build-linux-musl.yml index 5f92b71a2..06673f06a 100644 --- a/.github/workflows/build-linux-musl.yml +++ b/.github/workflows/build-linux-musl.yml @@ -54,9 +54,7 @@ jobs: ghcr.io/dart-musl/dart <<'EOF' set -e dart pub get - dart run grinder pkg-standalone-linux-${{ matrix.arch }} - # Rename the artifact from -linux- to -linux-musl- to avoid conflict with glibc builds. - find build -name '*.tar.gz' -print0 | xargs -0 -n 1 -- sh -xc 'mv "$1" "$(echo "$1" | sed -e "s/linux/linux-musl/")"' -- + dart run grinder pkg-standalone-linux-${{ matrix.arch }}-musl EOF - name: Upload Artifact diff --git a/CHANGELOG.md b/CHANGELOG.md index bb67ee905..4f2090551 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,14 @@ +## 1.71.1 + +### Command-Line Interface + +* Ship the musl Linux release with the proper Dart executable. + +### Embedded Sass + +* In the JS Embedded Host, properly install the musl Linux embedded compiler + when running on musl Linux. + ## 1.71.0 For more information about `pkg:` importers, see [the diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index a0a629ba1..02ada6ee5 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.4.1 + +* No user-visible changes. + ## 9.4.0 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index ce05d56df..452c66f3b 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: 9.4.0 +version: 9.4.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.71.0 + sass: 1.71.1 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index c3a1596b4..72e075f54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.71.0 +version: 1.71.1-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From 85a932f6481896f3de4b9359b0b531fe28b49e45 Mon Sep 17 00:00:00 2001 From: Jonny Gerig Meyer Date: Tue, 20 Feb 2024 18:00:31 -0500 Subject: [PATCH 23/35] Add missing ESM export of NodePackageImporter (#2177) Co-authored-by: Natalie Weizenbaum --- CHANGELOG.md | 4 ++++ tool/grind.dart | 1 + 2 files changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4f2090551..f45e9f6d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ * Ship the musl Linux release with the proper Dart executable. +### JavaScript API + +* Export the `NodePackageImporter` class in ESM mode. + ### Embedded Sass * In the JS Embedded Host, properly install the musl Linux embedded compiler diff --git a/tool/grind.dart b/tool/grind.dart index fd6792926..c7ab27bf8 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -84,6 +84,7 @@ void main(List args) { 'FALSE', 'NULL', 'types', + 'NodePackageImporter', }; pkg.githubReleaseNotes.fn = () => From 6d66c4376ad352ec4521b775c3559e1828e839ea Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 20 Feb 2024 17:20:35 -0800 Subject: [PATCH 24/35] Properly handle `new NodePackageImporter()` with an ESM entrypoint (#2181) Closes #2178 --- CHANGELOG.md | 8 ++++++++ lib/src/importer/node_package.dart | 8 ++------ lib/src/js/compile.dart | 13 ++++++++----- lib/src/js/module.dart | 26 ++++++++++++++++++++++++++ lib/src/js/utils.dart | 25 +++++++++++++++++++++---- tool/grind.dart | 2 ++ 6 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 lib/src/js/module.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index f45e9f6d3..ddf9cbc85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ * Export the `NodePackageImporter` class in ESM mode. +* Allow `NodePackageImporter` to locate a default directory even when the + entrypoint is an ESM module. + +### Dart API + +* Make passing a null argument to `NodePackageImporter()` a static error rather + than just a runtime error. + ### Embedded Sass * In the JS Embedded Host, properly install the musl Linux embedded compiler diff --git a/lib/src/importer/node_package.dart b/lib/src/importer/node_package.dart index ff7b51ca7..5a81baf3b 100644 --- a/lib/src/importer/node_package.dart +++ b/lib/src/importer/node_package.dart @@ -19,12 +19,8 @@ class NodePackageImporter extends Importer { late final String _entryPointDirectory; /// Creates a Node package importer with the associated entry point. - NodePackageImporter(String? entryPointDirectory) { - if (entryPointDirectory == null) { - throw "The Node package importer cannot determine an entry point " - "because `require.main.filename` is not defined. " - "Please provide an `entryPointDirectory` to the `NodePackageImporter`."; - } else if (isBrowser) { + NodePackageImporter(String entryPointDirectory) { + if (isBrowser) { throw "The Node package importer cannot be used without a filesystem."; } _entryPointDirectory = p.absolute(entryPointDirectory); diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index 086123846..a08806975 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -334,10 +334,13 @@ List _parseFunctions(Object? functions, {bool asynch = false}) { final JSClass nodePackageImporterClass = () { var jsClass = createJSClass( 'sass.NodePackageImporter', - (Object self, [String? entryPointDirectory]) => NodePackageImporter( - entryPointDirectory ?? - (requireMainFilename != null - ? p.dirname(requireMainFilename!) - : null))); + (Object self, [String? entrypointDirectory]) => NodePackageImporter( + switch ((entrypointDirectory, entrypointFilename)) { + ((var directory?, _)) => directory, + (_, var filename?) => p.dirname(filename), + _ => throw "The Node package importer cannot determine an entry " + "point because `require.main.filename` is not defined. Please " + "provide an `entryPointDirectory` to the `NodePackageImporter`." + })); return jsClass; }(); diff --git a/lib/src/js/module.dart b/lib/src/js/module.dart new file mode 100644 index 000000000..0723b994e --- /dev/null +++ b/lib/src/js/module.dart @@ -0,0 +1,26 @@ +// 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 'package:js/js.dart'; + +@JS('nodeModule') +external JSModule get module; + +/// A Dart API for the [`node:module`] module. +/// +/// [`node:module`]: https://nodejs.org/api/module.html#modules-nodemodule-api +@JS() +@anonymous +class JSModule { + /// See https://nodejs.org/api/module.html#modulecreaterequirefilename. + external JSModuleRequire createRequire(String filename); +} + +/// A `require` function returned by `module.createRequire()`. +@JS() +@anonymous +class JSModuleRequire { + /// See https://nodejs.org/api/modules.html#requireresolverequest-options. + external String resolve(String filename); +} diff --git a/lib/src/js/utils.dart b/lib/src/js/utils.dart index 08fdd8f6b..844157676 100644 --- a/lib/src/js/utils.dart +++ b/lib/src/js/utils.dart @@ -5,7 +5,7 @@ import 'dart:js_util'; import 'dart:typed_data'; -import 'package:node_interop/js.dart'; +import 'package:node_interop/node.dart' hide module; import 'package:js/js.dart'; import 'package:js/js_util.dart'; @@ -14,6 +14,7 @@ import '../utils.dart'; import '../value.dart'; import 'array.dart'; import 'function.dart'; +import 'module.dart'; import 'reflection.dart'; import 'url.dart'; @@ -234,6 +235,22 @@ Syntax parseSyntax(String? syntax) => switch (syntax) { _ => jsThrow(JsError('Unknown syntax "$syntax".')) }; -/// The value of require.main.filename -@JS("require.main.filename") -external String? get requireMainFilename; +/// The path to the Node.js entrypoint, if one can be located. +String? get entrypointFilename { + if (_requireMain?.filename case var filename?) { + return filename; + } else if (process.argv case [_, String path]) { + return module.createRequire(path).resolve(path); + } else { + return null; + } +} + +@JS("require.main") +external _RequireMain? get _requireMain; + +@JS() +@anonymous +class _RequireMain { + external String? get filename; +} diff --git a/tool/grind.dart b/tool/grind.dart index c7ab27bf8..95d9b3cc8 100644 --- a/tool/grind.dart +++ b/tool/grind.dart @@ -35,6 +35,8 @@ void main(List args) { pkg.JSRequire("chokidar", target: pkg.JSRequireTarget.cli), pkg.JSRequire("readline", target: pkg.JSRequireTarget.cli), pkg.JSRequire("fs", target: pkg.JSRequireTarget.node), + pkg.JSRequire("module", + target: pkg.JSRequireTarget.node, identifier: 'nodeModule'), pkg.JSRequire("stream", target: pkg.JSRequireTarget.node), pkg.JSRequire("util", target: pkg.JSRequireTarget.node), ]; From 1b4d703ad369a2176f0bc0bd4a37502e7acd9c1d Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 20 Feb 2024 17:37:04 -0800 Subject: [PATCH 25/35] Release 1.71.1 (#2182) --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 72e075f54..35c938efb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.71.1-dev +version: 1.71.1 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From fd67fe678c92d1e646bbbb2a57a0a5d3cf89e8ca Mon Sep 17 00:00:00 2001 From: James Stuckey Weber Date: Mon, 4 Mar 2024 20:30:54 -0500 Subject: [PATCH 26/35] [Hotfix Node Package Importer]- Handle subpath without extensions (#2184) Co-authored-by: Natalie Weizenbaum --- CHANGELOG.md | 5 +++++ lib/src/importer/node_package.dart | 1 + pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ddf9cbc85..1eb735f4f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## 1.72.0 + +* Allow the Node.js `pkg:` importer to load Sass stylesheets for `package.json` + `exports` field entries without extensions. + ## 1.71.1 ### Command-Line Interface diff --git a/lib/src/importer/node_package.dart b/lib/src/importer/node_package.dart index 5a81baf3b..d621ccf94 100644 --- a/lib/src/importer/node_package.dart +++ b/lib/src/importer/node_package.dart @@ -354,6 +354,7 @@ class NodePackageImporter extends Importer { paths.add(subpath); } else { paths.addAll([ + subpath, '$subpath.scss', '$subpath.sass', '$subpath.css', diff --git a/pubspec.yaml b/pubspec.yaml index 35c938efb..3105413b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.71.1 +version: 1.72.0-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From fa4d909f92c46cf90d02f95d2e79ed781a497fec Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:44:05 -0700 Subject: [PATCH 27/35] Bump softprops/action-gh-release from 1 to 2 (#2191) Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 1 to 2. - [Release notes](https://github.com/softprops/action-gh-release/releases) - [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md) - [Commits](https://github.com/softprops/action-gh-release/compare/v1...v2) --- updated-dependencies: - dependency-name: softprops/action-gh-release dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b374f5995..7f7c60139 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,7 +53,7 @@ jobs: uses: actions/download-artifact@v4 - name: Release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: files: | build-*/* From 6e2d637ac3a0ef99c194c4ba393ed24f40a6cf09 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 12 Mar 2024 14:16:10 -0700 Subject: [PATCH 28/35] Allow adjacent forward slashes in plain CSS expressions (#2190) See sass/sass#3797 --- CHANGELOG.md | 3 +++ lib/src/parse/css.dart | 4 +++- lib/src/parse/parser.dart | 8 +++++--- lib/src/parse/stylesheet.dart | 10 ++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1eb735f4f..790613be0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 1.72.0 +* Support adjacent `/`s without whitespace in between when parsing plain CSS + expressions. + * Allow the Node.js `pkg:` importer to load Sass stylesheets for `package.json` `exports` field entries without extensions. diff --git a/lib/src/parse/css.dart b/lib/src/parse/css.dart index 754a3614a..747d22c49 100644 --- a/lib/src/parse/css.dart +++ b/lib/src/parse/css.dart @@ -32,7 +32,9 @@ class CssParser extends ScssParser { CssParser(super.contents, {super.url, super.logger}); - void silentComment() { + bool silentComment() { + if (inExpression) return false; + var start = scanner.state; super.silentComment(); error("Silent comments aren't allowed in plain CSS.", diff --git a/lib/src/parse/parser.dart b/lib/src/parse/parser.dart index 6dd17c80d..a16b871e7 100644 --- a/lib/src/parse/parser.dart +++ b/lib/src/parse/parser.dart @@ -114,8 +114,7 @@ class Parser { switch (scanner.peekChar(1)) { case $slash: - silentComment(); - return true; + return silentComment(); case $asterisk: loudComment(); return true; @@ -135,12 +134,15 @@ class Parser { } /// Consumes and ignores a silent (Sass-style) comment. + /// + /// Returns whether the comment was consumed. @protected - void silentComment() { + bool silentComment() { scanner.expect("//"); while (!scanner.isDone && !scanner.peekChar().isNewline) { scanner.readChar(); } + return true; } /// Consumes and ignores a loud (CSS-style) comment. diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index 15c568f66..d0c620f24 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -56,6 +56,11 @@ abstract class StylesheetParser extends Parser { /// Whether the parser is currently within a parenthesized expression. var _inParentheses = false; + /// Whether the parser is currently within an expression. + @protected + bool get inExpression => _inExpression; + var _inExpression = false; + /// A map from all variable names that are assigned with `!global` in the /// current stylesheet to the nodes where they're defined. /// @@ -1686,7 +1691,9 @@ abstract class StylesheetParser extends Parser { } var start = scanner.state; + var wasInExpression = _inExpression; var wasInParentheses = _inParentheses; + _inExpression = true; // We use the convention below of referring to nullable variables that are // shared across anonymous functions in this method with a trailing @@ -2039,11 +2046,13 @@ abstract class StylesheetParser extends Parser { _inParentheses = wasInParentheses; var singleExpression = singleExpression_; if (singleExpression != null) commaExpressions.add(singleExpression); + _inExpression = wasInExpression; return ListExpression(commaExpressions, ListSeparator.comma, scanner.spanFrom(beforeBracket ?? start), brackets: bracketList); } else if (bracketList && spaceExpressions != null) { resolveOperations(); + _inExpression = wasInExpression; return ListExpression(spaceExpressions..add(singleExpression_!), ListSeparator.space, scanner.spanFrom(beforeBracket!), brackets: true); @@ -2054,6 +2063,7 @@ abstract class StylesheetParser extends Parser { ListSeparator.undecided, scanner.spanFrom(beforeBracket!), brackets: true); } + _inExpression = wasInExpression; return singleExpression_!; } } From 48e2d0cb02f03e70a61fd91b4d1eedb3f46c077f Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 12 Mar 2024 14:20:25 -0700 Subject: [PATCH 29/35] Preserve underscores in `VariableExpression.toString()` (#2185) Closes #2180 --- CHANGELOG.md | 3 +++ lib/src/ast/sass/expression/variable.dart | 2 +- pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 790613be0..d0e3727ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,9 @@ * Allow the Node.js `pkg:` importer to load Sass stylesheets for `package.json` `exports` field entries without extensions. +* When printing suggestions for variables, use underscores in variable names + when the original usage used underscores. + ## 1.71.1 ### Command-Line Interface diff --git a/lib/src/ast/sass/expression/variable.dart b/lib/src/ast/sass/expression/variable.dart index c07ffbc5a..7a839d867 100644 --- a/lib/src/ast/sass/expression/variable.dart +++ b/lib/src/ast/sass/expression/variable.dart @@ -35,5 +35,5 @@ final class VariableExpression implements Expression, SassReference { T accept(ExpressionVisitor visitor) => visitor.visitVariableExpression(this); - String toString() => namespace == null ? '\$$name' : '$namespace.\$$name'; + String toString() => span.text; } diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index 02ada6ee5..a444830d7 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.4.2 + +* No user-visible changes. + ## 9.4.1 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index 452c66f3b..b2c6b87bb 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: 9.4.1 +version: 9.4.2 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.71.1 + sass: 1.71.2 dev_dependencies: dartdoc: ^6.0.0 From 033049102b76a1cb3b82999938714058321fc33b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 12 Mar 2024 14:21:32 -0700 Subject: [PATCH 30/35] Update to node 20 (#2192) --- .github/util/initialize/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/util/initialize/action.yml b/.github/util/initialize/action.yml index 30252d356..ea8c2f0cf 100644 --- a/.github/util/initialize/action.yml +++ b/.github/util/initialize/action.yml @@ -13,7 +13,7 @@ runs: sdk: "${{ inputs.dart-sdk }}" architecture: "${{ inputs.architecture }}" - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: "${{ inputs.node-version }}" @@ -23,7 +23,7 @@ runs: - run: npm install shell: bash - - uses: bufbuild/buf-setup-action@v1.13.1 + - uses: bufbuild/buf-setup-action@v1.30.0 with: {github_token: "${{ inputs.github-token }}"} - name: Check out the language repo From 9af6bbf8a0082c4ab6bdd2999db8a89b23817353 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Tue, 12 Mar 2024 14:34:56 -0700 Subject: [PATCH 31/35] Properly handle `pkg:` imports with args (#2193) Closes #2188 --- CHANGELOG.md | 5 +++++ lib/src/js/utils.dart | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0e3727ac..6397a7741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ * When printing suggestions for variables, use underscores in variable names when the original usage used underscores. +### JavaScript API + +* Properly resolve `pkg:` imports with the Node.js package importer when + arguments are passed to the JavaScript process. + ## 1.71.1 ### Command-Line Interface diff --git a/lib/src/js/utils.dart b/lib/src/js/utils.dart index 844157676..a6b269782 100644 --- a/lib/src/js/utils.dart +++ b/lib/src/js/utils.dart @@ -239,7 +239,7 @@ Syntax parseSyntax(String? syntax) => switch (syntax) { String? get entrypointFilename { if (_requireMain?.filename case var filename?) { return filename; - } else if (process.argv case [_, String path]) { + } else if (process.argv case [_, String path, ...]) { return module.createRequire(path).resolve(path); } else { return null; From ce16b35ca1f4d6cbc3528c54b58647de939f2a4b Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 13 Mar 2024 13:42:44 -0700 Subject: [PATCH 32/35] Cut a release (#2194) --- pkg/sass_api/CHANGELOG.md | 4 ++++ pkg/sass_api/pubspec.yaml | 4 ++-- pubspec.yaml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index a444830d7..bd7e9e54f 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,7 @@ +## 9.5.0 + +* No user-visible changes. + ## 9.4.2 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index b2c6b87bb..aa9b8eaae 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: 9.4.2 +version: 9.5.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.71.2 + sass: 1.72.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 3105413b4..ba9181c16 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.72.0-dev +version: 1.72.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From 772280a7ff0605e17d1a247c201f5865d001cdd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=AA=E3=81=A4=E3=81=8D?= Date: Tue, 19 Mar 2024 15:17:50 -0700 Subject: [PATCH 33/35] Support linux-riscv64 and windows-arm64 (#2201) Co-authored-by: Natalie Weizenbaum --- .github/workflows/build-android.yml | 4 ++++ .github/workflows/build-linux-musl.yml | 3 +++ .github/workflows/build-linux.yml | 4 ++++ .github/workflows/build-windows.yml | 6 ++---- CHANGELOG.md | 4 ++++ pubspec.yaml | 2 +- 6 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-android.yml b/.github/workflows/build-android.yml index 5f2bb57db..e8ccdd7dc 100644 --- a/.github/workflows/build-android.yml +++ b/.github/workflows/build-android.yml @@ -26,6 +26,10 @@ jobs: - arch: arm lib: lib platform: linux/arm64 + # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + - arch: riscv64 + lib: lib64 + platform: linux/amd64 # linux/riscv64 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-linux-musl.yml b/.github/workflows/build-linux-musl.yml index 06673f06a..5f4ba2639 100644 --- a/.github/workflows/build-linux-musl.yml +++ b/.github/workflows/build-linux-musl.yml @@ -26,6 +26,9 @@ jobs: # https://gitlab.com/qemu-project/qemu/-/issues/1729 - arch: arm platform: linux/amd64 # linux/arm/v7 + # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + - arch: riscv64 + platform: linux/amd64 # linux/riscv64 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index e36628c4a..c8f5ddd41 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -22,6 +22,10 @@ jobs: platform: linux/arm/v7 - arch: arm64 platform: linux/arm64 + # There is no docker image for riscv64 dart-sdk, build kernel snapshot instead. + # https://github.com/dart-lang/dart-docker/issues/96#issuecomment-1669860829 + - arch: riscv64 + platform: linux/amd64 # linux/riscv64 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index 7036dfa34..ba9ff8bb1 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -18,10 +18,8 @@ jobs: runner: windows-latest - arch: ia32 runner: windows-latest - # The support of windows-arm64 dart-sdk is in beta. - # TODO: Enable this once windows-arm64 support is stable. - # - arch: arm64 - # runner: windows-latest + - arch: arm64 + runner: windows-latest steps: - uses: actions/checkout@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 6397a7741..5bf0228ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.72.1 + +* Add linux-riscv64 and windows-arm64 releases. + ## 1.72.0 * Support adjacent `/`s without whitespace in between when parsing plain CSS diff --git a/pubspec.yaml b/pubspec.yaml index ba9181c16..89a24f5cd 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.72.0 +version: 1.72.1-dev description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From 9302b3519cec96e9b61511588081a498d6e0b38e Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Fri, 22 Mar 2024 15:01:28 -0700 Subject: [PATCH 34/35] Add support for nesting in plain CSS (#2198) See sass/sass#3524 Closes #1927 --- CHANGELOG.md | 5 +- lib/src/ast/css/modifiable/style_rule.dart | 3 +- lib/src/ast/css/style_rule.dart | 8 +++ lib/src/ast/selector/list.dart | 42 +++++++++------ lib/src/functions/selector.dart | 4 +- lib/src/parse/selector.dart | 40 ++++++++++---- lib/src/parse/stylesheet.dart | 56 +++++++++---------- lib/src/util/character.dart | 10 ---- lib/src/visitor/async_evaluate.dart | 61 ++++++++++++++------- lib/src/visitor/evaluate.dart | 63 ++++++++++++++-------- pkg/sass_api/CHANGELOG.md | 8 +++ pkg/sass_api/pubspec.yaml | 4 +- pubspec.yaml | 2 +- 13 files changed, 192 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf0228ac..3d430a121 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ -## 1.72.1 +## 1.73.0 + +* Add support for nesting in plain CSS files. This is not processed by Sass at + all; it's emitted exactly as-is in the CSS. * Add linux-riscv64 and windows-arm64 releases. diff --git a/lib/src/ast/css/modifiable/style_rule.dart b/lib/src/ast/css/modifiable/style_rule.dart index a5d2b1f0c..6e242d36c 100644 --- a/lib/src/ast/css/modifiable/style_rule.dart +++ b/lib/src/ast/css/modifiable/style_rule.dart @@ -21,12 +21,13 @@ final class ModifiableCssStyleRule extends ModifiableCssParentNode final SelectorList originalSelector; final FileSpan span; + final bool fromPlainCss; /// Creates a new [ModifiableCssStyleRule]. /// /// If [originalSelector] isn't passed, it defaults to [_selector.value]. ModifiableCssStyleRule(this._selector, this.span, - {SelectorList? originalSelector}) + {SelectorList? originalSelector, this.fromPlainCss = false}) : originalSelector = originalSelector ?? _selector.value; T accept(ModifiableCssVisitor visitor) => diff --git a/lib/src/ast/css/style_rule.dart b/lib/src/ast/css/style_rule.dart index ccce74fdb..8b9da6663 100644 --- a/lib/src/ast/css/style_rule.dart +++ b/lib/src/ast/css/style_rule.dart @@ -2,6 +2,8 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:meta/meta.dart'; + import '../selector.dart'; import 'node.dart'; @@ -16,4 +18,10 @@ abstract interface class CssStyleRule implements CssParentNode { /// The selector for this rule, before any extensions were applied. SelectorList get originalSelector; + + /// Whether this style rule was originally defined in a plain CSS stylesheet. + /// + /// :nodoc: + @internal + bool get fromPlainCss; } diff --git a/lib/src/ast/selector/list.dart b/lib/src/ast/selector/list.dart index 4fc24f7f6..8da4598f6 100644 --- a/lib/src/ast/selector/list.dart +++ b/lib/src/ast/selector/list.dart @@ -57,9 +57,10 @@ final class SelectorList extends Selector { /// Parses a selector list from [contents]. /// - /// If passed, [url] is the name of the file from which [contents] comes. - /// [allowParent] and [allowPlaceholder] control whether [ParentSelector]s or - /// [PlaceholderSelector]s are allowed in this selector, respectively. + /// If passed, [url] is the name of the file from which [contents] comes. If + /// [allowParent] is false, this doesn't allow [ParentSelector]s. If + /// [plainCss] is true, this parses the selector as plain CSS rather than + /// unresolved Sass. /// /// If passed, [interpolationMap] maps the text of [contents] back to the /// original location of the selector in the source file. @@ -70,13 +71,13 @@ final class SelectorList extends Selector { Logger? logger, InterpolationMap? interpolationMap, bool allowParent = true, - bool allowPlaceholder = true}) => + bool plainCss = false}) => SelectorParser(contents, url: url, logger: logger, interpolationMap: interpolationMap, allowParent: allowParent, - allowPlaceholder: allowPlaceholder) + plainCss: plainCss) .parse(); T accept(SelectorVisitor visitor) => visitor.visitSelectorList(this); @@ -95,17 +96,24 @@ final class SelectorList extends Selector { return contents.isEmpty ? null : SelectorList(contents, span); } - /// Returns a new list with all [ParentSelector]s replaced with [parent]. + /// Returns a new selector list that represents [this] nested within [parent]. /// - /// If [implicitParent] is true, this treats [ComplexSelector]s that don't - /// contain an explicit [ParentSelector] as though they began with one. + /// By default, this replaces [ParentSelector]s in [this] with [parent]. If + /// [preserveParentSelectors] is true, this instead preserves those selectors + /// as parent selectors. + /// + /// If [implicitParent] is true, this prepends [parent] to any + /// [ComplexSelector]s in this that don't contain explicit [ParentSelector]s, + /// or to _all_ [ComplexSelector]s if [preserveParentSelectors] is true. /// /// The given [parent] may be `null`, indicating that this has no parents. If /// so, this list is returned as-is if it doesn't contain any explicit - /// [ParentSelector]s. If it does, this throws a [SassScriptException]. - SelectorList resolveParentSelectors(SelectorList? parent, - {bool implicitParent = true}) { + /// [ParentSelector]s or if [preserveParentSelectors] is true. Otherwise, this + /// throws a [SassScriptException]. + SelectorList nestWithin(SelectorList? parent, + {bool implicitParent = true, bool preserveParentSelectors = false}) { if (parent == null) { + if (preserveParentSelectors) return this; var parentSelector = accept(const _ParentSelectorVisitor()); if (parentSelector == null) return this; throw SassException( @@ -114,7 +122,7 @@ final class SelectorList extends Selector { } return SelectorList(flattenVertically(components.map((complex) { - if (!_containsParentSelector(complex)) { + if (preserveParentSelectors || !_containsParentSelector(complex)) { if (!implicitParent) return [complex]; return parent.components.map((parentComplex) => parentComplex.concatenate(complex, complex.span)); @@ -122,7 +130,7 @@ final class SelectorList extends Selector { var newComplexes = []; for (var component in complex.components) { - var resolved = _resolveParentSelectorsCompound(component, parent); + var resolved = _nestWithinCompound(component, parent); if (resolved == null) { if (newComplexes.isEmpty) { newComplexes.add(ComplexSelector( @@ -165,7 +173,7 @@ final class SelectorList extends Selector { /// [ParentSelector]s replaced with [parent]. /// /// Returns `null` if [component] doesn't contain any [ParentSelector]s. - Iterable? _resolveParentSelectorsCompound( + Iterable? _nestWithinCompound( ComplexSelectorComponent component, SelectorList parent) { var simples = component.selector.components; var containsSelectorPseudo = simples.any((simple) { @@ -181,8 +189,8 @@ final class SelectorList extends Selector { ? simples.map((simple) => switch (simple) { PseudoSelector(:var selector?) when _containsParentSelector(selector) => - simple.withSelector(selector.resolveParentSelectors(parent, - implicitParent: false)), + simple.withSelector( + selector.nestWithin(parent, implicitParent: false)), _ => simple }) : simples; @@ -261,6 +269,8 @@ final class SelectorList extends Selector { /// Returns a copy of `this` with [combinators] added to the end of each /// complex selector in [components]. + /// + /// @nodoc @internal SelectorList withAdditionalCombinators( List> combinators) => diff --git a/lib/src/functions/selector.dart b/lib/src/functions/selector.dart index d6fddf9de..08e133dbf 100644 --- a/lib/src/functions/selector.dart +++ b/lib/src/functions/selector.dart @@ -52,7 +52,7 @@ final _nest = _function("nest", r"$selectors...", (arguments) { first = false; return result; }) - .reduce((parent, child) => child.resolveParentSelectors(parent)) + .reduce((parent, child) => child.nestWithin(parent)) .asSassList; }); @@ -83,7 +83,7 @@ final _append = _function("append", r"$selectors...", (arguments) { ...rest ], span); }), span) - .resolveParentSelectors(parent); + .nestWithin(parent); }).asSassList; }); diff --git a/lib/src/parse/selector.dart b/lib/src/parse/selector.dart index 0e5b7a04f..97a334166 100644 --- a/lib/src/parse/selector.dart +++ b/lib/src/parse/selector.dart @@ -31,17 +31,24 @@ class SelectorParser extends Parser { /// Whether this parser allows the parent selector `&`. final bool _allowParent; - /// Whether this parser allows placeholder selectors beginning with `%`. - final bool _allowPlaceholder; + /// Whether to parse the selector as plain CSS. + final bool _plainCss; + /// Creates a parser that parses CSS selectors. + /// + /// If [allowParent] is `false`, this will throw a [SassFormatException] if + /// the selector includes the parent selector `&`. + /// + /// If [plainCss] is `true`, this will parse the selector as a plain CSS + /// selector rather than a Sass selector. SelectorParser(super.contents, {super.url, super.logger, super.interpolationMap, bool allowParent = true, - bool allowPlaceholder = true}) + bool plainCss = false}) : _allowParent = allowParent, - _allowPlaceholder = allowPlaceholder; + _plainCss = plainCss; SelectorList parse() { return wrapSpanFormatException(() { @@ -165,7 +172,9 @@ class SelectorParser extends Parser { } } - if (lastCompound != null) { + if (combinators.isNotEmpty && _plainCss) { + scanner.error("expected selector."); + } else if (lastCompound != null) { components.add(ComplexSelectorComponent( lastCompound, combinators, spanFrom(componentStart))); } else if (combinators.isNotEmpty) { @@ -184,8 +193,8 @@ class SelectorParser extends Parser { var start = scanner.state; var components = [_simpleSelector()]; - while (isSimpleSelectorStart(scanner.peekChar())) { - components.add(_simpleSelector(allowParent: false)); + while (_isSimpleSelectorStart(scanner.peekChar())) { + components.add(_simpleSelector(allowParent: _plainCss)); } return CompoundSelector(components, spanFrom(start)); @@ -207,8 +216,8 @@ class SelectorParser extends Parser { return _idSelector(); case $percent: var selector = _placeholderSelector(); - if (!_allowPlaceholder) { - error("Placeholder selectors aren't allowed here.", + if (_plainCss) { + error("Placeholder selectors aren't allowed in plain CSS.", scanner.spanFrom(start)); } return selector; @@ -340,6 +349,11 @@ class SelectorParser extends Parser { var start = scanner.state; scanner.expectChar($ampersand); var suffix = lookingAtIdentifierBody() ? identifierBody() : null; + if (_plainCss && suffix != null) { + scanner.error("Parent selectors can't have suffixes in plain CSS.", + position: start.position, length: scanner.position - start.position); + } + return ParentSelector(spanFrom(start), suffix: suffix); } @@ -457,4 +471,12 @@ class SelectorParser extends Parser { spanFrom(start)); } } + + // Returns whether [character] can start a simple selector in the middle of a + // compound selector. + bool _isSimpleSelectorStart(int? character) => switch (character) { + $asterisk || $lbracket || $dot || $hash || $percent || $colon => true, + $ampersand => _plainCss, + _ => false + }; } diff --git a/lib/src/parse/stylesheet.dart b/lib/src/parse/stylesheet.dart index d0c620f24..82f3f4565 100644 --- a/lib/src/parse/stylesheet.dart +++ b/lib/src/parse/stylesheet.dart @@ -324,10 +324,6 @@ abstract class StylesheetParser extends Parser { /// parsed as a selector and never as a property with nested properties /// beneath it. Statement _declarationOrStyleRule() { - if (plainCss && _inStyleRule && !_inUnknownAtRule) { - return _propertyOrVariableDeclaration(); - } - // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. @@ -400,10 +396,7 @@ abstract class StylesheetParser extends Parser { } var postColonWhitespace = rawText(whitespace); - if (lookingAtChildren()) { - return _withChildren(_declarationChild, start, - (children, span) => Declaration.nested(name, children, span)); - } + if (_tryDeclarationChildren(name, start) case var nested?) return nested; midBuffer.write(postColonWhitespace); var couldBeSelector = @@ -439,12 +432,8 @@ abstract class StylesheetParser extends Parser { return nameBuffer; } - if (lookingAtChildren()) { - return _withChildren( - _declarationChild, - start, - (children, span) => - Declaration.nested(name, children, span, value: value)); + if (_tryDeclarationChildren(name, start, value: value) case var nested?) { + return nested; } else { expectStatementSeparator(); return Declaration(name, value, scanner.spanFrom(start)); @@ -549,31 +538,36 @@ abstract class StylesheetParser extends Parser { } whitespace(); - - if (lookingAtChildren()) { - if (plainCss) { - scanner.error("Nested declarations aren't allowed in plain CSS."); - } - return _withChildren(_declarationChild, start, - (children, span) => Declaration.nested(name, children, span)); - } + if (_tryDeclarationChildren(name, start) case var nested?) return nested; var value = _expression(); - if (lookingAtChildren()) { - if (plainCss) { - scanner.error("Nested declarations aren't allowed in plain CSS."); - } - return _withChildren( - _declarationChild, - start, - (children, span) => - Declaration.nested(name, children, span, value: value)); + if (_tryDeclarationChildren(name, start, value: value) case var nested?) { + return nested; } else { expectStatementSeparator(); return Declaration(name, value, scanner.spanFrom(start)); } } + /// 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 + /// nesting. + Declaration? _tryDeclarationChildren( + Interpolation name, LineScannerState start, + {Expression? value}) { + if (!lookingAtChildren()) return null; + if (plainCss) { + scanner.error("Nested declarations aren't allowed in plain CSS."); + } + return _withChildren( + _declarationChild, + start, + (children, span) => + Declaration.nested(name, children, span, value: value)); + } + /// Consumes a statement that's allowed within a declaration. Statement _declarationChild() => scanner.peekChar() == $at ? _declarationAtRule() diff --git a/lib/src/util/character.dart b/lib/src/util/character.dart index ea4085d29..7141be67a 100644 --- a/lib/src/util/character.dart +++ b/lib/src/util/character.dart @@ -92,16 +92,6 @@ int combineSurrogates(int highSurrogate, int lowSurrogate) => // high/low surrogates. 0x10000 + ((highSurrogate & 0x3FF) << 10) + (lowSurrogate & 0x3FF); -// Returns whether [character] can start a simple selector other than a type -// selector. -bool isSimpleSelectorStart(int? character) => - character == $asterisk || - character == $lbracket || - character == $dot || - character == $hash || - character == $percent || - character == $colon; - /// Returns whether [identifier] is module-private. /// /// Assumes [identifier] is a valid Sass identifier. diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 26679cd8e..ce4105bda 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -2011,16 +2011,32 @@ final class _EvaluateVisitor } var parsedSelector = SelectorList.parse(selectorText, - interpolationMap: selectorMap, - allowParent: !_stylesheet.plainCss, - allowPlaceholder: !_stylesheet.plainCss, - logger: _logger) - .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + interpolationMap: selectorMap, + plainCss: _stylesheet.plainCss, + logger: _logger); + + var nest = !(_styleRule?.fromPlainCss ?? false); + if (nest) { + if (_stylesheet.plainCss) { + for (var complex in parsedSelector.components) { + if (complex.leadingCombinators case [var first, ...] + when _stylesheet.plainCss) { + throw _exception( + "Top-level leading combinators aren't allowed in plain CSS.", + first.span); + } + } + } + + parsedSelector = parsedSelector.nestWithin( + _styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: _stylesheet.plainCss); + } var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: parsedSelector); + originalSelector: parsedSelector, fromPlainCss: _stylesheet.plainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; await _withParent(rule, () async { @@ -2030,7 +2046,7 @@ final class _EvaluateVisitor } }); }, - through: (node) => node is CssStyleRule, + through: nest ? (node) => node is CssStyleRule : null, scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; @@ -2048,13 +2064,15 @@ final class _EvaluateVisitor complex.span.trimRight(), Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { - _warn( - 'The selector "${complex.toString().trim()}" is invalid CSS.\n' - 'This will be an error in Dart Sass 2.0.0.\n' - '\n' - 'More info: https://sass-lang.com/d/bogus-combinators', - complex.span.trimRight(), - Deprecation.bogusCombinators); + if (!_stylesheet.plainCss) { + _warn( + 'The selector "${complex.toString().trim()}" is invalid CSS.\n' + 'This will be an error in Dart Sass 2.0.0.\n' + '\n' + 'More info: https://sass-lang.com/d/bogus-combinators', + complex.span.trimRight(), + Deprecation.bogusCombinators); + } } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -3386,12 +3404,15 @@ final class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.resolveParentSelectors( - styleRule?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + var nest = !(_styleRule?.fromPlainCss ?? false); + var originalSelector = nest + ? node.selector.nestWithin(styleRule?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: node.fromPlainCss) + : node.selector; var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: originalSelector); + originalSelector: originalSelector, fromPlainCss: node.fromPlainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; await _withParent(rule, () async { @@ -3400,7 +3421,7 @@ final class _EvaluateVisitor await child.accept(this); } }); - }, through: (node) => node is CssStyleRule, scopeWhen: false); + }, through: nest ? (node) => node is CssStyleRule : null, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; if (_parent.children case [..., var lastChild] when styleRule == null) { diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index 096ae21fe..32c4e2764 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: 7351193aa9229e1434c09a2cbc9fa596cd924901 +// Checksum: 05cb957cd0c7698d8ad648f31d862dc91f0daa7b // // ignore_for_file: unused_import @@ -2001,16 +2001,32 @@ final class _EvaluateVisitor } var parsedSelector = SelectorList.parse(selectorText, - interpolationMap: selectorMap, - allowParent: !_stylesheet.plainCss, - allowPlaceholder: !_stylesheet.plainCss, - logger: _logger) - .resolveParentSelectors(_styleRuleIgnoringAtRoot?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + interpolationMap: selectorMap, + plainCss: _stylesheet.plainCss, + logger: _logger); + + var nest = !(_styleRule?.fromPlainCss ?? false); + if (nest) { + if (_stylesheet.plainCss) { + for (var complex in parsedSelector.components) { + if (complex.leadingCombinators case [var first, ...] + when _stylesheet.plainCss) { + throw _exception( + "Top-level leading combinators aren't allowed in plain CSS.", + first.span); + } + } + } + + parsedSelector = parsedSelector.nestWithin( + _styleRuleIgnoringAtRoot?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: _stylesheet.plainCss); + } var selector = _extensionStore.addSelector(parsedSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: parsedSelector); + originalSelector: parsedSelector, fromPlainCss: _stylesheet.plainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; _withParent(rule, () { @@ -2020,7 +2036,7 @@ final class _EvaluateVisitor } }); }, - through: (node) => node is CssStyleRule, + through: nest ? (node) => node is CssStyleRule : null, scopeWhen: node.hasDeclarations); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; @@ -2038,13 +2054,15 @@ final class _EvaluateVisitor complex.span.trimRight(), Deprecation.bogusCombinators); } else if (complex.leadingCombinators.isNotEmpty) { - _warn( - 'The selector "${complex.toString().trim()}" is invalid CSS.\n' - 'This will be an error in Dart Sass 2.0.0.\n' - '\n' - 'More info: https://sass-lang.com/d/bogus-combinators', - complex.span.trimRight(), - Deprecation.bogusCombinators); + if (!_stylesheet.plainCss) { + _warn( + 'The selector "${complex.toString().trim()}" is invalid CSS.\n' + 'This will be an error in Dart Sass 2.0.0.\n' + '\n' + 'More info: https://sass-lang.com/d/bogus-combinators', + complex.span.trimRight(), + Deprecation.bogusCombinators); + } } else { _warn( 'The selector "${complex.toString().trim()}" is only valid for ' @@ -3354,12 +3372,15 @@ final class _EvaluateVisitor } var styleRule = _styleRule; - var originalSelector = node.selector.resolveParentSelectors( - styleRule?.originalSelector, - implicitParent: !_atRootExcludingStyleRule); + var nest = !(_styleRule?.fromPlainCss ?? false); + var originalSelector = nest + ? node.selector.nestWithin(styleRule?.originalSelector, + implicitParent: !_atRootExcludingStyleRule, + preserveParentSelectors: node.fromPlainCss) + : node.selector; var selector = _extensionStore.addSelector(originalSelector, _mediaQueries); var rule = ModifiableCssStyleRule(selector, node.span, - originalSelector: originalSelector); + originalSelector: originalSelector, fromPlainCss: node.fromPlainCss); var oldAtRootExcludingStyleRule = _atRootExcludingStyleRule; _atRootExcludingStyleRule = false; _withParent(rule, () { @@ -3368,7 +3389,7 @@ final class _EvaluateVisitor child.accept(this); } }); - }, through: (node) => node is CssStyleRule, scopeWhen: false); + }, through: nest ? (node) => node is CssStyleRule : null, scopeWhen: false); _atRootExcludingStyleRule = oldAtRootExcludingStyleRule; if (_parent.children case [..., var lastChild] when styleRule == null) { diff --git a/pkg/sass_api/CHANGELOG.md b/pkg/sass_api/CHANGELOG.md index bd7e9e54f..6e8d05390 100644 --- a/pkg/sass_api/CHANGELOG.md +++ b/pkg/sass_api/CHANGELOG.md @@ -1,3 +1,11 @@ +## 10.0.0 + +* Remove the `allowPlaceholders` argument from `SelectorList.parse()`. Instead, + it now has a more generic `plainCss` argument which tells it to parse the + selector in plain CSS mode. + +* Rename `SelectorList.resolveParentSelectors` to `SelectorList.nestWithin`. + ## 9.5.0 * No user-visible changes. diff --git a/pkg/sass_api/pubspec.yaml b/pkg/sass_api/pubspec.yaml index aa9b8eaae..4780522de 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: 9.5.0 +version: 10.0.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.72.0 + sass: 1.73.0 dev_dependencies: dartdoc: ^6.0.0 diff --git a/pubspec.yaml b/pubspec.yaml index 89a24f5cd..52c8c58df 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: sass -version: 1.72.1-dev +version: 1.73.0 description: A Sass implementation in Dart. homepage: https://github.com/sass/dart-sass From c8d064368c29a93b4c10a34be472f3d497457858 Mon Sep 17 00:00:00 2001 From: Natalie Weizenbaum Date: Wed, 27 Mar 2024 11:03:17 -0700 Subject: [PATCH 35/35] Better handle filesystem importers when load paths aren't necessary (#2203) See #2199 See sass/sass#3815 --- CHANGELOG.md | 23 +++++++ bin/sass.dart | 3 +- lib/src/deprecation.dart | 5 ++ lib/src/evaluation_context.dart | 35 ++++++---- lib/src/executable/compile_stylesheet.dart | 4 +- lib/src/importer/filesystem.dart | 74 +++++++++++++++++++--- test/cli/shared/update.dart | 13 ++++ test/dart_api/logger_test.dart | 4 -- 8 files changed, 133 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d430a121..c5ae69c7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,31 @@ * Add support for nesting in plain CSS files. This is not processed by Sass at all; it's emitted exactly as-is in the CSS. +* In certain circumstances, the current working directory was unintentionally + being made available as a load path. This is now deprecated. Anyone relying on + this should explicitly pass in `.` as a load path or `FilesystemImporter('.')` + as the current importer. + * Add linux-riscv64 and windows-arm64 releases. +### Command-Line Interface + +* Fix a bug where absolute `file:` URLs weren't loaded for files compiled via + the command line unless an unrelated load path was also passed. + +* Fix a bug where `--update` would always update files that were specified via + absolute path unless an unrelated load path was also passed. + +### Dart API + +* Add `FilesystemImporter.noLoadPath`, which is a `FilesystemImporter` that can + load absolute `file:` URLs and resolve URLs relative to the base file but + doesn't load relative URLs from a load path. + +* `FilesystemImporter.cwd` is now deprecated. Either use + `FilesystemImporter.noLoadPath` if you weren't intending to rely on the load + path, or `FilesystemImporter('.')` if you were. + ## 1.72.0 * Support adjacent `/`s without whitespace in between when parsing plain CSS diff --git a/bin/sass.dart b/bin/sass.dart index 67b1782ed..d24439eb9 100644 --- a/bin/sass.dart +++ b/bin/sass.dart @@ -13,6 +13,7 @@ import 'package:sass/src/executable/options.dart'; import 'package:sass/src/executable/repl.dart'; 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/stylesheet_graph.dart'; @@ -45,7 +46,7 @@ Future main(List args) async { } var graph = StylesheetGraph(ImportCache( - importers: options.pkgImporters, + importers: [...options.pkgImporters, FilesystemImporter.noLoadPath], loadPaths: options.loadPaths, // This logger is only used for handling fatal/future deprecations // during parsing, and is re-used across parses, so we don't want to diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 2724d0afe..3f8540634 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -69,6 +69,11 @@ enum Deprecation { deprecatedIn: '1.62.3', description: 'Passing null as alpha in the ${isJS ? 'JS' : 'Dart'} API.'), + fsImporterCwd('fs-importer-cwd', + deprecatedIn: '1.73.0', + description: + 'Using the current working directory as an implicit load path.'), + @Deprecated('This deprecation name was never actually used.') calcInterp('calc-interp', deprecatedIn: null), diff --git a/lib/src/evaluation_context.dart b/lib/src/evaluation_context.dart index 5c1b074a9..30b3852f9 100644 --- a/lib/src/evaluation_context.dart +++ b/lib/src/evaluation_context.dart @@ -7,6 +7,7 @@ import 'dart:async'; import 'package:source_span/source_span.dart'; import 'deprecation.dart'; +import 'logger.dart'; /// An interface that exposes information about the current Sass evaluation. /// @@ -17,13 +18,16 @@ abstract interface class EvaluationContext { /// /// Throws [StateError] if there isn't a Sass stylesheet currently being /// evaluated. - static EvaluationContext get current { - if (Zone.current[#_evaluationContext] case EvaluationContext context) { - return context; - } else { - throw StateError("No Sass stylesheet is currently being evaluated."); - } - } + static EvaluationContext get current => + currentOrNull ?? + (throw StateError("No Sass stylesheet is currently being evaluated.")); + + /// The current evaluation context, or `null` if none exists. + static EvaluationContext? get currentOrNull => + switch (Zone.current[#_evaluationContext]) { + EvaluationContext context => context, + _ => null + }; /// Returns the span for the currently executing callable. /// @@ -50,13 +54,20 @@ abstract interface class EvaluationContext { /// This may only be called within a custom function or importer callback. /// {@category Compile} void warn(String message, {bool deprecation = false}) => - EvaluationContext.current - .warn(message, deprecation ? Deprecation.userAuthored : null); + switch (EvaluationContext.currentOrNull) { + var context? => + context.warn(message, deprecation ? Deprecation.userAuthored : null), + _ when deprecation => (const Logger.stderr()) + .warnForDeprecation(Deprecation.userAuthored, message), + _ => (const Logger.stderr()).warn(message) + }; /// Prints a deprecation warning with [message] of type [deprecation]. -void warnForDeprecation(String message, Deprecation deprecation) { - EvaluationContext.current.warn(message, deprecation); -} +void warnForDeprecation(String message, Deprecation deprecation) => + switch (EvaluationContext.currentOrNull) { + var context? => context.warn(message, deprecation), + _ => (const Logger.stderr()).warnForDeprecation(deprecation, message) + }; /// Runs [callback] with [context] as [EvaluationContext.current]. /// diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 160a01d9a..cd121a6f5 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -73,8 +73,8 @@ Future _compileStylesheetWithoutErrorHandling(ExecutableOptions options, try { if (source != null && destination != null && - !graph.modifiedSince( - p.toUri(source), modificationTime(destination), importer)) { + !graph.modifiedSince(p.toUri(p.absolute(source)), + modificationTime(destination), importer)) { return; } } on FileSystemException catch (_) { diff --git a/lib/src/importer/filesystem.dart b/lib/src/importer/filesystem.dart index 47b0ae288..cb23d3095 100644 --- a/lib/src/importer/filesystem.dart +++ b/lib/src/importer/filesystem.dart @@ -5,30 +5,86 @@ import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; +import '../deprecation.dart'; +import '../evaluation_context.dart'; import '../importer.dart'; import '../io.dart' as io; import '../syntax.dart'; import '../util/nullable.dart'; import 'utils.dart'; -/// An importer that loads files from a load path on the filesystem. +/// An importer that loads files from a load path on the filesystem, either +/// relative to the path passed to [FilesystemImporter.new] or absolute `file:` +/// URLs. +/// +/// Use [FilesystemImporter.noLoadPath] to _only_ load absolute `file:` URLs and +/// URLs relative to the current file. /// /// {@category Importer} @sealed class FilesystemImporter extends Importer { /// The path relative to which this importer looks for files. - final String _loadPath; + /// + /// If this is `null`, this importer will _only_ load absolute `file:` URLs + /// and URLs relative to the current file. + final String? _loadPath; + + /// Whether loading from files from this importer's [_loadPath] is deprecated. + final bool _loadPathDeprecated; /// Creates an importer that loads files relative to [loadPath]. - FilesystemImporter(String loadPath) : _loadPath = p.absolute(loadPath); + FilesystemImporter(String loadPath) + : _loadPath = p.absolute(loadPath), + _loadPathDeprecated = false; + + FilesystemImporter._deprecated(String loadPath) + : _loadPath = p.absolute(loadPath), + _loadPathDeprecated = true; + + /// Creates an importer that _only_ loads absolute `file:` URLs and URLs + /// relative to the current file. + FilesystemImporter._noLoadPath() + : _loadPath = null, + _loadPathDeprecated = false; - /// Creates an importer relative to the current working directory. - static final cwd = FilesystemImporter('.'); + /// A [FilesystemImporter] that loads files relative to the current working + /// directory. + /// + /// Historically, this was the best default for supporting `file:` URL loads + /// when the load path didn't matter. However, adding the current working + /// directory to the load path wasn't always desirable, so it's no longer + /// recommended. Instead, either use [FilesystemImporter.noLoadPath] if the + /// load path doesn't matter, or `FilesystemImporter('.')` to explicitly + /// preserve the existing behavior. + @Deprecated( + "Use FilesystemImporter.noLoadPath or FilesystemImporter('.') instead.") + static final cwd = FilesystemImporter._deprecated('.'); + + /// Creates an importer that _only_ loads absolute `file:` URLsand URLs + /// relative to the current file. + static final noLoadPath = FilesystemImporter._noLoadPath(); Uri? canonicalize(Uri url) { - if (url.scheme != 'file' && url.scheme != '') return null; - return resolveImportPath(p.join(_loadPath, p.fromUri(url))) - .andThen((resolved) => p.toUri(io.canonicalize(resolved))); + String? resolved; + if (url.scheme == 'file') { + resolved = resolveImportPath(p.fromUri(url)); + } else if (url.scheme != '') { + return null; + } else if (_loadPath case var loadPath?) { + resolved = resolveImportPath(p.join(loadPath, p.fromUri(url))); + + if (resolved != null && _loadPathDeprecated) { + warnForDeprecation( + "Using the current working directory as an implicit load path is " + "deprecated. Either add it as an explicit load path or importer, or " + "load this stylesheet from a different URL.", + Deprecation.fsImporterCwd); + } + } else { + return null; + } + + return resolved.andThen((resolved) => p.toUri(io.canonicalize(resolved))); } ImporterResult? load(Uri url) { @@ -53,5 +109,5 @@ class FilesystemImporter extends Importer { basename == p.url.withoutExtension(canonicalBasename); } - String toString() => _loadPath; + String toString() => _loadPath ?? ''; } diff --git a/test/cli/shared/update.dart b/test/cli/shared/update.dart index 4fc7bfbc1..73b8a21df 100644 --- a/test/cli/shared/update.dart +++ b/test/cli/shared/update.dart @@ -2,6 +2,7 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. +import 'package:path/path.dart' as p; import 'package:test/test.dart'; import 'package:test_descriptor/test_descriptor.dart' as d; import 'package:test_process/test_process.dart'; @@ -148,6 +149,18 @@ void sharedTests(Future runSass(Iterable arguments)) { await d.file("out.css", "x {y: z}").validate(); }); + // Regression test for #2203 + test("whose sources weren't modified with an absolute path", () async { + await d.file("test.scss", "a {b: c}").create(); + await d.file("out.css", "x {y: z}").create(); + + var sass = await update(["${p.absolute(d.path('test.scss'))}:out.css"]); + expect(sass.stdout, emitsDone); + await sass.shouldExit(0); + + await d.file("out.css", "x {y: z}").validate(); + }); + test("whose sibling was modified", () async { await d.file("test1.scss", "a {b: c}").create(); await d.file("out1.css", "x {y: z}").create(); diff --git a/test/dart_api/logger_test.dart b/test/dart_api/logger_test.dart index a1c0ec93f..979b2ba11 100644 --- a/test/dart_api/logger_test.dart +++ b/test/dart_api/logger_test.dart @@ -227,10 +227,6 @@ void main() { mustBeCalled(); })); }); - - test("throws an error outside a callback", () { - expect(() => warn("heck"), throwsStateError); - }); }); }