diff --git a/lib/sass.dart b/lib/sass.dart index 91f69d1eb..6036224f8 100644 --- a/lib/sass.dart +++ b/lib/sass.dart @@ -16,6 +16,7 @@ import 'src/import_cache.dart'; import 'src/importer.dart'; import 'src/importer/utils.dart'; import 'src/logger.dart'; +import 'src/source_map_include_sources.dart'; import 'src/syntax.dart'; import 'src/util/nullable.dart'; import 'src/visitor/serialize.dart'; @@ -26,6 +27,7 @@ export 'src/deprecation.dart'; export 'src/exception.dart' show SassException; export 'src/importer.dart'; export 'src/logger.dart' show Logger; +export 'src/source_map_include_sources.dart'; export 'src/syntax.dart'; export 'src/value.dart' hide @@ -90,6 +92,9 @@ export 'src/evaluation_context.dart' show warn; /// /// [`source_maps`]: https://pub.dartlang.org/packages/source_maps /// +/// The [sourceMapIncludeSources] parameter controls the ways in which the +/// compiler can choose to include source contents in the source map. +/// /// If [charset] is `true`, this will include a `@charset` declaration or a /// UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII /// characters. Otherwise, it will never include a `@charset` declaration or a @@ -112,6 +117,8 @@ CompileResult compileToResult( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -130,6 +137,7 @@ CompileResult compileToResult( quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources, charset: charset, silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, @@ -199,6 +207,9 @@ CompileResult compileToResult( /// /// [`source_maps`]: https://pub.dartlang.org/packages/source_maps /// +/// The [sourceMapIncludeSources] parameter controls the ways in which the +/// compiler can choose to include source contents in the source map. +/// /// If [charset] is `true`, this will include a `@charset` declaration or a /// UTF-8 [byte-order mark][] if the stylesheet contains any non-ASCII /// characters. Otherwise, it will never include a `@charset` declaration or a @@ -224,6 +235,8 @@ CompileResult compileStringToResult( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -245,6 +258,7 @@ CompileResult compileStringToResult( quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources, charset: charset, silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, @@ -268,6 +282,8 @@ Future compileToResultAsync( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -286,6 +302,7 @@ Future compileToResultAsync( quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources, charset: charset, silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, @@ -314,6 +331,8 @@ Future compileStringToResultAsync( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -335,6 +354,7 @@ Future compileStringToResultAsync( quietDeps: quietDeps, verbose: verbose, sourceMap: sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources, charset: charset, silenceDeprecations: silenceDeprecations, fatalDeprecations: fatalDeprecations, diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index cfea91a55..c697f3de1 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -2,8 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:convert'; - import 'package:cli_pkg/js.dart'; import 'package:path/path.dart' as p; @@ -18,8 +16,8 @@ import 'importer/no_op.dart'; import 'io.dart'; import 'logger.dart'; import 'logger/deprecation_processing.dart'; +import 'source_map_include_sources.dart'; import 'syntax.dart'; -import 'utils.dart'; import 'visitor/async_evaluate.dart'; import 'visitor/serialize.dart'; @@ -42,6 +40,8 @@ Future compileAsync( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -88,6 +88,7 @@ Future compileAsync( lineFeed, quietDeps, sourceMap, + sourceMapIncludeSources, charset, ); @@ -117,6 +118,8 @@ Future compileStringAsync( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -156,6 +159,7 @@ Future compileStringAsync( lineFeed, quietDeps, sourceMap, + sourceMapIncludeSources, charset, ); @@ -179,6 +183,7 @@ Future _compileStylesheet( LineFeed? lineFeed, bool quietDeps, bool sourceMap, + SourceMapIncludeSources sourceMapIncludeSources, bool charset, ) async { if (nodeImporter != null) { @@ -188,6 +193,23 @@ Future _compileStylesheet( 'Dart Sass 2.0.0.\n\n' 'More info: https://sass-lang.com/d/legacy-js-api', ); + } else { + if (sourceMapIncludeSources == SourceMapIncludeSources.true_ || + sourceMapIncludeSources == SourceMapIncludeSources.false_) { + var boolean = sourceMapIncludeSources == SourceMapIncludeSources.true_; + var suggestion = boolean ? 'always' : 'never'; + logger?.warnForDeprecation( + Deprecation.sourceMapIncludeSourcesBoolean, + 'Passing a boolean value for Options.sourceMapIncludeSources is ' + 'deprecated and will be removed in Dart Sass 2.0.0.\n' + "Please use '$suggestion' instead of $boolean.\n\n" + 'More info: https://sass-lang.com/d/source-map-include-sources-boolean', + ); + sourceMapIncludeSources = + sourceMapIncludeSources == SourceMapIncludeSources.true_ + ? SourceMapIncludeSources.always + : SourceMapIncludeSources.never; + } } var evaluateResult = await evaluateAsync( stylesheet, @@ -212,16 +234,22 @@ Future _compileStylesheet( ); var resultSourceMap = serializeResult.sourceMap; - if (resultSourceMap != null && importCache != null) { - mapInPlace( - resultSourceMap.urls, - (url) => url == '' - ? Uri.dataFromString( - stylesheet.span.file.getText(0), - encoding: utf8, - ).toString() - : importCache.sourceMapUrl(Uri.parse(url)).toString(), - ); + if (resultSourceMap != null) { + if (importCache != null) { + for (var i = 0, length = resultSourceMap.urls.length; i < length; i++) { + var canonicalUrl = Uri.parse(resultSourceMap.urls[i]); + var sourceMapUrl = importCache.sourceMapUrlOrNull(canonicalUrl); + if (sourceMapUrl != null) { + resultSourceMap.urls[i] = sourceMapUrl.toString(); + if (sourceMapIncludeSources == SourceMapIncludeSources.auto) { + resultSourceMap.files[i] = null; + } + } + } + } + if (sourceMapIncludeSources == SourceMapIncludeSources.never) { + resultSourceMap.files.fillRange(0, resultSourceMap.files.length, null); + } } return CompileResult(evaluateResult, serializeResult); diff --git a/lib/src/async_import_cache.dart b/lib/src/async_import_cache.dart index baa512beb..b242f1412 100644 --- a/lib/src/async_import_cache.dart +++ b/lib/src/async_import_cache.dart @@ -371,9 +371,16 @@ final class AsyncImportCache { /// Returns the URL to use in the source map to refer to [canonicalUrl]. /// /// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache. + @Deprecated('Use sourceMapUrlOrNull instead.') Uri sourceMapUrl(Uri canonicalUrl) => _resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl; + /// Returns the URL to use in the source map to refer to [canonicalUrl]. + /// + /// Returns `null` if it hasn't been loaded by this cache. + Uri? sourceMapUrlOrNull(Uri canonicalUrl) => + _resultsCache[canonicalUrl]?.sourceMapUrl; + /// Returns the most recent time the stylesheet at [canonicalUrl] was loaded /// from its importer, or `null` if it has never been loaded. @internal diff --git a/lib/src/compile.dart b/lib/src/compile.dart index 785643008..f02f34089 100644 --- a/lib/src/compile.dart +++ b/lib/src/compile.dart @@ -5,14 +5,12 @@ // DO NOT EDIT. This file was generated from async_compile.dart. // See tool/grind/synchronize.dart for details. // -// Checksum: d305a0f75e329a29f5aff734ac31ce145fd3b8d5 +// Checksum: 1ff55074bb0b91ced8c0ad971324a48a71a8213a // // ignore_for_file: unused_import export 'async_compile.dart'; -import 'dart:convert'; - import 'package:cli_pkg/js.dart'; import 'package:path/path.dart' as p; @@ -27,8 +25,8 @@ import 'importer/no_op.dart'; import 'io.dart'; import 'logger.dart'; import 'logger/deprecation_processing.dart'; +import 'source_map_include_sources.dart'; import 'syntax.dart'; -import 'utils.dart'; import 'visitor/evaluate.dart'; import 'visitor/serialize.dart'; @@ -51,6 +49,8 @@ CompileResult compile( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -97,6 +97,7 @@ CompileResult compile( lineFeed, quietDeps, sourceMap, + sourceMapIncludeSources, charset, ); @@ -126,6 +127,8 @@ CompileResult compileString( bool quietDeps = false, bool verbose = false, bool sourceMap = false, + SourceMapIncludeSources sourceMapIncludeSources = + SourceMapIncludeSources.always, bool charset = true, Iterable? silenceDeprecations, Iterable? fatalDeprecations, @@ -165,6 +168,7 @@ CompileResult compileString( lineFeed, quietDeps, sourceMap, + sourceMapIncludeSources, charset, ); @@ -188,6 +192,7 @@ CompileResult _compileStylesheet( LineFeed? lineFeed, bool quietDeps, bool sourceMap, + SourceMapIncludeSources sourceMapIncludeSources, bool charset, ) { if (nodeImporter != null) { @@ -197,6 +202,23 @@ CompileResult _compileStylesheet( 'Dart Sass 2.0.0.\n\n' 'More info: https://sass-lang.com/d/legacy-js-api', ); + } else { + if (sourceMapIncludeSources == SourceMapIncludeSources.true_ || + sourceMapIncludeSources == SourceMapIncludeSources.false_) { + var boolean = sourceMapIncludeSources == SourceMapIncludeSources.true_; + var suggestion = boolean ? 'always' : 'never'; + logger?.warnForDeprecation( + Deprecation.sourceMapIncludeSourcesBoolean, + 'Passing a boolean value for Options.sourceMapIncludeSources is ' + 'deprecated and will be removed in Dart Sass 2.0.0.\n' + "Please use '$suggestion' instead of $boolean.\n\n" + 'More info: https://sass-lang.com/d/source-map-include-sources-boolean', + ); + sourceMapIncludeSources = + sourceMapIncludeSources == SourceMapIncludeSources.true_ + ? SourceMapIncludeSources.always + : SourceMapIncludeSources.never; + } } var evaluateResult = evaluate( stylesheet, @@ -221,16 +243,22 @@ CompileResult _compileStylesheet( ); var resultSourceMap = serializeResult.sourceMap; - if (resultSourceMap != null && importCache != null) { - mapInPlace( - resultSourceMap.urls, - (url) => url == '' - ? Uri.dataFromString( - stylesheet.span.file.getText(0), - encoding: utf8, - ).toString() - : importCache.sourceMapUrl(Uri.parse(url)).toString(), - ); + if (resultSourceMap != null) { + if (importCache != null) { + for (var i = 0, length = resultSourceMap.urls.length; i < length; i++) { + var canonicalUrl = Uri.parse(resultSourceMap.urls[i]); + var sourceMapUrl = importCache.sourceMapUrlOrNull(canonicalUrl); + if (sourceMapUrl != null) { + resultSourceMap.urls[i] = sourceMapUrl.toString(); + if (sourceMapIncludeSources == SourceMapIncludeSources.auto) { + resultSourceMap.files[i] = null; + } + } + } + } + if (sourceMapIncludeSources == SourceMapIncludeSources.never) { + resultSourceMap.files.fillRange(0, resultSourceMap.files.length, null); + } } return CompileResult(evaluateResult, serializeResult); diff --git a/lib/src/deprecation.dart b/lib/src/deprecation.dart index 5672078a1..88cab6474 100644 --- a/lib/src/deprecation.dart +++ b/lib/src/deprecation.dart @@ -15,7 +15,7 @@ enum Deprecation { // DO NOT EDIT. This section was generated from the language repo. // See tool/grind/generate_deprecations.dart for details. // - // Checksum: 6fc524360d067b73c243c666e27a9a9ea7e08841 + // Checksum: d6dc089a2bcab991dd117a6592f2f55d1caafccd /// Deprecation for passing a string directly to meta.call(). callString('call-string', @@ -151,6 +151,12 @@ enum Deprecation { deprecatedIn: '1.95.0', description: 'The Sass if(\$condition, \$if-true, \$if-false) function.'), + /// Deprecation for passing a boolean value to as Options.sourceMapIncludeSources. + sourceMapIncludeSourcesBoolean('source-map-include-sources-boolean', + deprecatedIn: '1.99.0', + description: + 'Passing a boolean value to as Options.sourceMapIncludeSources.'), + // END AUTOGENERATED CODE /// Used for deprecations coming from user-authored code. diff --git a/lib/src/embedded/compilation_dispatcher.dart b/lib/src/embedded/compilation_dispatcher.dart index 190583890..5e4fff041 100644 --- a/lib/src/embedded/compilation_dispatcher.dart +++ b/lib/src/embedded/compilation_dispatcher.dart @@ -15,6 +15,7 @@ import 'package:sass/sass.dart' as sass; import 'package:sass/src/importer/node_package.dart' as npi; import '../logger.dart'; +import '../util/source_map.dart'; import '../value/function.dart'; import '../value/mixin.dart'; import 'embedded_sass.pb.dart'; @@ -164,6 +165,14 @@ final class CompilationDispatcher { (signature) => hostCallable(this, functions, mixins, signature), ); + var sourceMapIncludeSources = switch (request.sourceMapIncludeSources) { + SourceMapIncludeSources.AUTO => sass.SourceMapIncludeSources.auto, + SourceMapIncludeSources.ALWAYS => sass.SourceMapIncludeSources.always, + SourceMapIncludeSources.NEVER => sass.SourceMapIncludeSources.never, + _ => + throw "Unknown SourceMapIncludeSources ${request.sourceMapIncludeSources}.", + }; + late sass.CompileResult result; switch (request.whichInput()) { case InboundMessage_CompileRequest_Input.string: @@ -185,6 +194,7 @@ final class CompilationDispatcher { silenceDeprecations: silenceDeprecations, futureDeprecations: futureDeprecations, sourceMap: request.sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources, charset: request.charset, ); @@ -207,6 +217,7 @@ final class CompilationDispatcher { silenceDeprecations: silenceDeprecations, futureDeprecations: futureDeprecations, sourceMap: request.sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources, charset: request.charset, ); } on FileSystemException catch (error) { @@ -230,11 +241,8 @@ final class CompilationDispatcher { var sourceMap = result.sourceMap; if (sourceMap != null) { - success.sourceMap = json.encode( - sourceMap.toJson( - includeSourceContents: request.sourceMapIncludeSources, - ), - ); + success.sourceMap = json.encode(sourceMapToJson(sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources)); } return OutboundMessage_CompileResponse() ..success = success diff --git a/lib/src/executable/compile_stylesheet.dart b/lib/src/executable/compile_stylesheet.dart index 62ea7568e..71f0153a0 100644 --- a/lib/src/executable/compile_stylesheet.dart +++ b/lib/src/executable/compile_stylesheet.dart @@ -16,6 +16,7 @@ import '../importer/filesystem.dart'; import '../io.dart'; import '../stylesheet_graph.dart'; import '../syntax.dart'; +import '../util/source_map.dart'; import '../utils.dart'; import '../visitor/serialize.dart'; import 'options.dart'; @@ -131,6 +132,7 @@ Future _compileStylesheetWithoutErrorHandling( quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, + sourceMapIncludeSources: options.sourceMapIncludeSources, charset: options.charset, silenceDeprecations: options.silenceDeprecations, fatalDeprecations: options.fatalDeprecations, @@ -145,6 +147,7 @@ Future _compileStylesheetWithoutErrorHandling( quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, + sourceMapIncludeSources: options.sourceMapIncludeSources, charset: options.charset, silenceDeprecations: options.silenceDeprecations, fatalDeprecations: options.fatalDeprecations, @@ -166,6 +169,7 @@ Future _compileStylesheetWithoutErrorHandling( quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, + sourceMapIncludeSources: options.sourceMapIncludeSources, charset: options.charset, silenceDeprecations: options.silenceDeprecations, fatalDeprecations: options.fatalDeprecations, @@ -180,6 +184,7 @@ Future _compileStylesheetWithoutErrorHandling( quietDeps: options.quietDeps, verbose: options.verbose, sourceMap: options.emitSourceMap, + sourceMapIncludeSources: options.sourceMapIncludeSources, charset: options.charset, silenceDeprecations: options.silenceDeprecations, fatalDeprecations: options.fatalDeprecations, @@ -246,15 +251,12 @@ String _writeSourceMap( sourceMap.targetUrl = p.toUri(p.basename(destination)).toString(); } - // TODO(nweiz): Don't explicitly use a type parameter when dart-lang/sdk#25490 - // is fixed. - mapInPlace( + mapInPlace( sourceMap.urls, (url) => options.sourceMapUrl(Uri.parse(url), destination).toString(), ); - var sourceMapText = jsonEncode( - sourceMap.toJson(includeSourceContents: options.embedSources), - ); + var sourceMapText = jsonEncode(sourceMapToJson(sourceMap, + sourceMapIncludeSources: options.sourceMapIncludeSources)); Uri url; if (options.embedSourceMap) { diff --git a/lib/src/executable/options.dart b/lib/src/executable/options.dart index 2c901b665..a14463731 100644 --- a/lib/src/executable/options.dart +++ b/lib/src/executable/options.dart @@ -528,7 +528,10 @@ final class ExecutableOptions { bool get embedSourceMap => _options['embed-source-map'] as bool; /// Whether to embed the source files in the generated source map. - bool get embedSources => _options['embed-sources'] as bool; + SourceMapIncludeSources get sourceMapIncludeSources => + _options['embed-sources'] as bool + ? SourceMapIncludeSources.always + : SourceMapIncludeSources.never; /// Parses options from [args]. /// @@ -554,7 +557,9 @@ final class ExecutableOptions { /// /// If [url] isn't a `file:` URL, returns it as-is. Uri sourceMapUrl(Uri url, String? destination) { - if (url.scheme.isNotEmpty && url.scheme != 'file') return url; + if (url.scheme.isNotEmpty && url.scheme != 'file' || url.toString() == '') { + return url; + } var path = p.fromUri(url); return p.toUri( diff --git a/lib/src/import_cache.dart b/lib/src/import_cache.dart index b8600e6a7..2244a805b 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: bcdb7643d7bd5740fdb4586869f1b4cd362cf902 +// Checksum: 104c7704a424d58a3071f5d29aea7a42d99aef3b // // ignore_for_file: unused_import @@ -368,9 +368,16 @@ final class ImportCache { /// Returns the URL to use in the source map to refer to [canonicalUrl]. /// /// Returns [canonicalUrl] as-is if it hasn't been loaded by this cache. + @Deprecated('Use sourceMapUrlOrNull instead.') Uri sourceMapUrl(Uri canonicalUrl) => _resultsCache[canonicalUrl]?.sourceMapUrl ?? canonicalUrl; + /// Returns the URL to use in the source map to refer to [canonicalUrl]. + /// + /// Returns `null` if it hasn't been loaded by this cache. + Uri? sourceMapUrlOrNull(Uri canonicalUrl) => + _resultsCache[canonicalUrl]?.sourceMapUrl; + /// Returns the most recent time the stylesheet at [canonicalUrl] was loaded /// from its importer, or `null` if it has never been loaded. @internal diff --git a/lib/src/importer/result.dart b/lib/src/importer/result.dart index 8cbb1e0d3..884c8c567 100644 --- a/lib/src/importer/result.dart +++ b/lib/src/importer/result.dart @@ -2,8 +2,6 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:convert'; - import 'package:meta/meta.dart'; import '../importer.dart'; @@ -17,15 +15,8 @@ class ImporterResult { /// The contents of the stylesheet. final String contents; - /// An absolute, browser-accessible URL indicating the resolved location of - /// the imported stylesheet. - /// - /// This should be a `file:` URL if one is available, but an `http:` URL is - /// acceptable as well. If no URL is supplied, a `data:` URL is generated - /// automatically from [contents]. - Uri get sourceMapUrl => - _sourceMapUrl ?? Uri.dataFromString(contents, encoding: utf8); - final Uri? _sourceMapUrl; + /// An absolute URL indicating the resolved location of the imported stylesheet. + final Uri? sourceMapUrl; /// The syntax to use to parse the stylesheet. final Syntax syntax; @@ -40,11 +31,10 @@ class ImporterResult { /// parameter instead. ImporterResult( this.contents, { - Uri? sourceMapUrl, + this.sourceMapUrl, Syntax? syntax, @Deprecated("Use the syntax parameter instead.") bool? indented, - }) : _sourceMapUrl = sourceMapUrl, - syntax = syntax ?? (indented == true ? Syntax.sass : Syntax.scss) { + }) : syntax = syntax ?? (indented == true ? Syntax.sass : Syntax.scss) { if (sourceMapUrl?.scheme == '') { throw ArgumentError.value( sourceMapUrl, diff --git a/lib/src/js/compile.dart b/lib/src/js/compile.dart index fbb952d87..03c34da92 100644 --- a/lib/src/js/compile.dart +++ b/lib/src/js/compile.dart @@ -18,6 +18,7 @@ import '../importer/node_package.dart'; import '../io.dart'; import '../logger/js_to_dart.dart'; import '../util/nullable.dart'; +import '../util/source_map.dart'; import 'compile_options.dart'; import 'compile_result.dart'; import 'deprecations.dart'; @@ -42,6 +43,8 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { ascii: ascii, ); try { + var sourceMapIncludeSources = + parseSourceMapIncludeSources(options?.sourceMapIncludeSources); var result = compileToResult( path, color: color, @@ -51,6 +54,7 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, logger: logger, importers: options?.importers?.map(_parseImporter), functions: _parseFunctions(options?.functions).cast(), @@ -70,7 +74,7 @@ NodeCompileResult compile(String path, [CompileOptions? options]) { ); return _convertResult( result, - includeSourceContents: options?.sourceMapIncludeSources ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, ); } on SassException catch (error, stackTrace) { throwNodeException(error, color: color, ascii: ascii, trace: stackTrace); @@ -90,6 +94,8 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) { ascii: ascii, ); try { + var sourceMapIncludeSources = + parseSourceMapIncludeSources(options?.sourceMapIncludeSources); var result = compileStringToResult( text, syntax: parseSyntax(options?.syntax), @@ -101,6 +107,7 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, logger: logger, importers: options?.importers?.map(_parseImporter), importer: options?.importer.andThen(_parseImporter) ?? @@ -122,7 +129,7 @@ NodeCompileResult compileString(String text, [CompileStringOptions? options]) { ); return _convertResult( result, - includeSourceContents: options?.sourceMapIncludeSources ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, ); } on SassException catch (error, stackTrace) { throwNodeException(error, color: color, ascii: ascii, trace: stackTrace); @@ -146,6 +153,8 @@ Promise compileAsync(String path, [CompileOptions? options]) { ); return _wrapAsyncSassExceptions( futureToPromise(() async { + var sourceMapIncludeSources = + parseSourceMapIncludeSources(options?.sourceMapIncludeSources); var result = await compileToResultAsync( path, color: color, @@ -155,6 +164,7 @@ Promise compileAsync(String path, [CompileOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, logger: logger, importers: options?.importers?.map( (importer) => _parseAsyncImporter(importer), @@ -176,7 +186,7 @@ Promise compileAsync(String path, [CompileOptions? options]) { ); return _convertResult( result, - includeSourceContents: options?.sourceMapIncludeSources ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, ); }()), color: color, @@ -198,6 +208,8 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) { ); return _wrapAsyncSassExceptions( futureToPromise(() async { + var sourceMapIncludeSources = + parseSourceMapIncludeSources(options?.sourceMapIncludeSources); var result = await compileStringToResultAsync( text, syntax: parseSyntax(options?.syntax), @@ -209,6 +221,7 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) { verbose: options?.verbose ?? false, charset: options?.charset ?? true, sourceMap: options?.sourceMap ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, logger: logger, importers: options?.importers?.map( (importer) => _parseAsyncImporter(importer), @@ -234,7 +247,7 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) { ); return _convertResult( result, - includeSourceContents: options?.sourceMapIncludeSources ?? false, + sourceMapIncludeSources: sourceMapIncludeSources, ); }()), color: color, @@ -243,28 +256,28 @@ Promise compileStringAsync(String text, [CompileStringOptions? options]) { } /// Converts a Dart [CompileResult] into a JS API [NodeCompileResult]. -NodeCompileResult _convertResult( - CompileResult result, { - required bool includeSourceContents, -}) { - var sourceMap = result.sourceMap?.toJson( - includeSourceContents: includeSourceContents, - ); - if (sourceMap is Map && !sourceMap.containsKey('sources')) { +NodeCompileResult _convertResult(CompileResult result, + {required SourceMapIncludeSources sourceMapIncludeSources}) { + var loadedUrls = toJSArray(result.loadedUrls.map(dartToJSUrl)); + var sourceMap = result.sourceMap; + if (sourceMap == null) { + // The JS API tests expects *no* source map here, not a null source map. + return NodeCompileResult(css: result.css, loadedUrls: loadedUrls); + } + + var sourceMapJson = sourceMapToJson(sourceMap, + sourceMapIncludeSources: sourceMapIncludeSources); + if (!sourceMapJson.containsKey('sources')) { // Dart's source map library can omit the sources key, but JS's type // declaration doesn't allow that. - sourceMap['sources'] = []; + sourceMapJson['sources'] = []; } - var loadedUrls = toJSArray(result.loadedUrls.map(dartToJSUrl)); - return sourceMap == null - // The JS API tests expects *no* source map here, not a null source map. - ? NodeCompileResult(css: result.css, loadedUrls: loadedUrls) - : NodeCompileResult( - css: result.css, - loadedUrls: loadedUrls, - sourceMap: jsify(sourceMap), - ); + return NodeCompileResult( + css: result.css, + loadedUrls: loadedUrls, + sourceMap: jsify(sourceMapJson), + ); } /// Catches `SassException`s thrown by [promise] and rethrows them as JS API diff --git a/lib/src/js/compile_options.dart b/lib/src/js/compile_options.dart index c53a1893d..290fccc0d 100644 --- a/lib/src/js/compile_options.dart +++ b/lib/src/js/compile_options.dart @@ -19,7 +19,7 @@ class CompileOptions { external bool? get verbose; external bool? get charset; external bool? get sourceMap; - external bool? get sourceMapIncludeSources; + external Object? get sourceMapIncludeSources; external JSLogger? get logger; external List? get importers; external Object? get functions; diff --git a/lib/src/js/legacy.dart b/lib/src/js/legacy.dart index ae1b280b8..48f9b84b4 100644 --- a/lib/src/js/legacy.dart +++ b/lib/src/js/legacy.dart @@ -24,6 +24,7 @@ import '../logger.dart'; import '../logger/js_to_dart.dart'; import '../syntax.dart'; import '../util/nullable.dart'; +import '../util/source_map.dart'; import '../utils.dart'; import '../value.dart'; import '../visitor/serialize.dart'; @@ -118,6 +119,8 @@ Future _renderAsync(RenderOptions options) async { verbose: options.verbose ?? false, charset: options.charset ?? true, sourceMap: _enableSourceMaps(options), + sourceMapIncludeSources: + parseSourceMapIncludeSources(options.sourceMapContents), logger: logger, ); } else if (file != null) { @@ -145,6 +148,8 @@ Future _renderAsync(RenderOptions options) async { verbose: options.verbose ?? false, charset: options.charset ?? true, sourceMap: _enableSourceMaps(options), + sourceMapIncludeSources: + parseSourceMapIncludeSources(options.sourceMapContents), logger: logger, ); } else { @@ -202,6 +207,8 @@ RenderResult renderSync(RenderOptions options) { verbose: options.verbose ?? false, charset: options.charset ?? true, sourceMap: _enableSourceMaps(options), + sourceMapIncludeSources: + parseSourceMapIncludeSources(options.sourceMapContents), logger: logger, ); } else if (file != null) { @@ -232,6 +239,8 @@ RenderResult renderSync(RenderOptions options) { verbose: options.verbose ?? false, charset: options.charset ?? true, sourceMap: _enableSourceMaps(options), + sourceMapIncludeSources: + parseSourceMapIncludeSources(options.sourceMapContents), logger: logger, ); } else { @@ -506,8 +515,10 @@ RenderResult _newRenderResult( sourceMap.urls[i] = p.url.relative(source, from: sourceMapDirUrl); } - var json = sourceMap.toJson( - includeSourceContents: isTruthy(options.sourceMapContents), + var json = sourceMapToJson( + sourceMap, + sourceMapIncludeSources: + parseSourceMapIncludeSources(options.sourceMapContents), ); sourceMapBytes = utf8Encode(jsonEncode(json)); diff --git a/lib/src/js/utils.dart b/lib/src/js/utils.dart index 194ee4eab..2aa655f9a 100644 --- a/lib/src/js/utils.dart +++ b/lib/src/js/utils.dart @@ -9,6 +9,7 @@ import 'package:node_interop/node.dart' hide module; import 'package:js/js.dart'; import 'package:js/js_util.dart'; +import '../source_map_include_sources.dart'; import '../syntax.dart'; import '../utils.dart'; import '../util/map.dart'; @@ -271,6 +272,18 @@ Syntax parseSyntax(String? syntax) => switch (syntax) { _ => jsThrow(JsError('Unknown syntax "$syntax".')), }; +SourceMapIncludeSources parseSourceMapIncludeSources( + Object? sourceMapIncludeSources) => + switch (sourceMapIncludeSources) { + null || 'auto' => SourceMapIncludeSources.auto, + 'always' => SourceMapIncludeSources.always, + 'never' => SourceMapIncludeSources.never, + true => SourceMapIncludeSources.true_, + false => SourceMapIncludeSources.false_, + _ => jsThrow(JsError( + 'Unknown sourceMapIncludeSources "$sourceMapIncludeSources".')), + }; + /// The path to the Node.js entrypoint, if one can be located. String? get entrypointFilename { if (_requireMain?.filename case var filename?) { diff --git a/lib/src/source_map_include_sources.dart b/lib/src/source_map_include_sources.dart new file mode 100644 index 000000000..2bef87484 --- /dev/null +++ b/lib/src/source_map_include_sources.dart @@ -0,0 +1,23 @@ +// Copyright 2025 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. + +/// An enum of sourceMapIncludeSources options. +/// +/// {@category Compile} +enum SourceMapIncludeSources { + /// Let compiler decide whether to include each source content. + auto, + + /// Always include source contents. + always, + + /// Never include source contents. + never, + + @Deprecated('Use always instead.') + true_, + + @Deprecated('Use never instead.') + false_, +} diff --git a/lib/src/util/source_map.dart b/lib/src/util/source_map.dart new file mode 100644 index 000000000..556211bc0 --- /dev/null +++ b/lib/src/util/source_map.dart @@ -0,0 +1,21 @@ +// Copyright 2025 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:source_maps/source_maps.dart'; + +import '../source_map_include_sources.dart'; + +Map sourceMapToJson(SingleMapping sourceMap, + {required SourceMapIncludeSources sourceMapIncludeSources}) { + if (sourceMapIncludeSources == SourceMapIncludeSources.true_) { + sourceMapIncludeSources = SourceMapIncludeSources.always; + } else if (sourceMapIncludeSources == SourceMapIncludeSources.false_) { + sourceMapIncludeSources = SourceMapIncludeSources.never; + } + return sourceMap.toJson( + includeSourceContents: + sourceMapIncludeSources == SourceMapIncludeSources.always || + (sourceMapIncludeSources == SourceMapIncludeSources.auto && + sourceMap.files.any((file) => file != null))); +} diff --git a/test/cli/shared/source_maps.dart b/test/cli/shared/source_maps.dart index d6f22a9b0..1dc409800 100644 --- a/test/cli/shared/source_maps.dart +++ b/test/cli/shared/source_maps.dart @@ -170,7 +170,7 @@ void sharedTests(Future runSass(Iterable arguments)) { }); }); - test("with --stdin uses a data: URL", () async { + test("with --stdin uses an empty URL", () async { var sass = await runSass(["--stdin", "out.css"]); sass.stdin.writeln("a {b: c}"); sass.stdin.close(); @@ -178,9 +178,7 @@ void sharedTests(Future runSass(Iterable arguments)) { expect( _readJson("out.css.map"), - containsPair("sources", [ - Uri.dataFromString("a {b: c}\n", encoding: utf8).toString(), - ]), + containsPair("sources", ['']), ); }); diff --git a/test/dart_api/importer_test.dart b/test/dart_api/importer_test.dart index c7e7aa474..406841bd8 100644 --- a/test/dart_api/importer_test.dart +++ b/test/dart_api/importer_test.dart @@ -5,8 +5,6 @@ @TestOn('vm') library; -import 'dart:convert'; - import 'package:test/test.dart'; import 'package:sass/sass.dart'; @@ -305,7 +303,7 @@ void main() { expect(result.sourceMap!.urls, contains("u:blue")); }); - test("uses a data: source map URL if the importer doesn't provide one", () { + test("uses the canonical URL if the importer doesn't provide one", () { var result = compileStringToResult( '@use "orange";', importers: [ @@ -320,10 +318,7 @@ void main() { expect( result.sourceMap!.urls, contains( - Uri.dataFromString( - ".orange {color: orange}", - encoding: utf8, - ).toString(), + "u:orange", ), ); }); diff --git a/test/embedded/importer_test.dart b/test/embedded/importer_test.dart index de036c6d4..e649fc6ad 100644 --- a/test/embedded/importer_test.dart +++ b/test/embedded/importer_test.dart @@ -648,7 +648,8 @@ void main() { await process.close(); }); - test("uses a data: URL rather than an empty source map URL", () async { + test("uses the canonical URL rather than an empty source map URL", + () async { process.send( compileString( "@use 'other'", @@ -673,7 +674,7 @@ void main() { "a { b: c; }", sourceMap: (String map) { var mapping = source_maps.parse(map) as source_maps.SingleMapping; - expect(mapping.urls, [startsWith("data:")]); + expect(mapping.urls, contains("custom:other")); }, ); await process.close(); diff --git a/test/embedded/protocol_test.dart b/test/embedded/protocol_test.dart index 5a0a41a12..f9e22da37 100644 --- a/test/embedded/protocol_test.dart +++ b/test/embedded/protocol_test.dart @@ -297,7 +297,7 @@ void main() { expect(span.start.column, equals(3)); expect(span.end, equals(span.start)); expect(mapping, isA()); - expect((mapping as source_maps.SingleMapping).files[0], isNull); + expect((mapping as source_maps.SingleMapping).files[0], isNotNull); return true; }, ); @@ -305,13 +305,13 @@ void main() { }); test( - "includes a source map without content if source_map is true and source_map_include_sources is false", + "includes a source map with content if source_map is true and source_map_include_sources is auto", () async { process.send( compileString( "a {b: 1px + 2px}", sourceMap: true, - sourceMapIncludeSources: false, + sourceMapIncludeSources: SourceMapIncludeSources.AUTO, ), ); await expectSuccess( @@ -324,7 +324,7 @@ void main() { expect(span.start.column, equals(3)); expect(span.end, equals(span.start)); expect(mapping, isA()); - expect((mapping as source_maps.SingleMapping).files[0], isNull); + expect((mapping as source_maps.SingleMapping).files[0], isNotNull); return true; }, ); @@ -333,13 +333,13 @@ void main() { ); test( - "includes a source map with content if source_map is true and source_map_include_sources is true", + "includes a source map with content if source_map is true and source_map_include_sources is always", () async { process.send( compileString( "a {b: 1px + 2px}", sourceMap: true, - sourceMapIncludeSources: true, + sourceMapIncludeSources: SourceMapIncludeSources.ALWAYS, ), ); await expectSuccess( @@ -360,6 +360,34 @@ void main() { }, ); + test( + "includes a source map with content if source_map is true and source_map_include_sources is never", + () async { + process.send( + compileString( + "a {b: 1px + 2px}", + sourceMap: true, + sourceMapIncludeSources: SourceMapIncludeSources.NEVER, + ), + ); + await expectSuccess( + process, + "a { b: 3px; }", + sourceMap: (String map) { + var mapping = source_maps.parse(map); + var span = mapping.spanFor(2, 5)!; + expect(span.start.line, equals(0)); + expect(span.start.column, equals(3)); + expect(span.end, equals(span.start)); + expect(mapping, isA()); + expect((mapping as source_maps.SingleMapping).files[0], isNull); + return true; + }, + ); + await process.close(); + }, + ); + group("emits a log event", () { group("for a @debug rule", () { test("with correct fields", () async { diff --git a/test/embedded/utils.dart b/test/embedded/utils.dart index 6de2611b6..0a8f5ee9c 100644 --- a/test/embedded/utils.dart +++ b/test/embedded/utils.dart @@ -26,7 +26,7 @@ InboundMessage compileString( OutputStyle? style, String? url, bool? sourceMap, - bool? sourceMapIncludeSources, + SourceMapIncludeSources? sourceMapIncludeSources, Iterable? importers, InboundMessage_CompileRequest_Importer? importer, Iterable? functions,