diff --git a/lib/src/async_compile.dart b/lib/src/async_compile.dart index 5b7541c5a..d01dfcc40 100644 --- a/lib/src/async_compile.dart +++ b/lib/src/async_compile.dart @@ -11,7 +11,7 @@ import 'async_import_cache.dart'; import 'callable.dart'; import 'compile_result.dart'; import 'importer.dart'; -import 'importer/node.dart'; +import 'importer/legacy_node.dart'; import 'io.dart'; import 'logger.dart'; import 'logger/terse.dart'; diff --git a/lib/src/compile.dart b/lib/src/compile.dart index d7a43fe3d..8e4f650cb 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: 1a1251aa9f7312612a64760f59803568bd09a07c +// Checksum: f8b5bf7eafbe3523ca4df1a6832e131c5c03986b // // ignore_for_file: unused_import @@ -20,7 +20,7 @@ import 'import_cache.dart'; import 'callable.dart'; import 'compile_result.dart'; import 'importer.dart'; -import 'importer/node.dart'; +import 'importer/legacy_node.dart'; import 'io.dart'; import 'logger.dart'; import 'logger/terse.dart'; diff --git a/lib/src/importer/node.dart b/lib/src/importer/legacy_node.dart similarity index 65% rename from lib/src/importer/node.dart rename to lib/src/importer/legacy_node.dart index a3a47e71f..83528fc26 100644 --- a/lib/src/importer/node.dart +++ b/lib/src/importer/legacy_node.dart @@ -2,4 +2,4 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -export 'node/interface.dart' if (dart.library.js) 'node/implementation.dart'; +export 'legacy_node/interface.dart' if (dart.library.js) 'legacy_node/implementation.dart'; diff --git a/lib/src/importer/node/implementation.dart b/lib/src/importer/legacy_node/implementation.dart similarity index 98% rename from lib/src/importer/node/implementation.dart rename to lib/src/importer/legacy_node/implementation.dart index fec916002..1d7ffcd78 100644 --- a/lib/src/importer/node/implementation.dart +++ b/lib/src/importer/legacy_node/implementation.dart @@ -10,10 +10,10 @@ import 'package:tuple/tuple.dart'; import '../../io.dart'; import '../../node/function.dart'; -import '../../node/importer_result.dart'; +import '../../node/legacy/importer_result.dart'; +import '../../node/legacy/render_context.dart'; import '../../node/utils.dart'; import '../../util/nullable.dart'; -import '../../node/render_context.dart'; import '../utils.dart'; /// An importer that encapsulates Node Sass's import logic. diff --git a/lib/src/importer/node/interface.dart b/lib/src/importer/legacy_node/interface.dart similarity index 100% rename from lib/src/importer/node/interface.dart rename to lib/src/importer/legacy_node/interface.dart diff --git a/lib/src/node.dart b/lib/src/node.dart index 0595be249..ef90b65ef 100644 --- a/lib/src/node.dart +++ b/lib/src/node.dart @@ -2,43 +2,21 @@ // MIT-style license that can be found in the LICENSE file or at // https://opensource.org/licenses/MIT. -import 'dart:async'; -import 'dart:convert'; -import 'dart:js_util'; -import 'dart:typed_data'; - import 'package:js/js.dart'; -import 'package:node_interop/js.dart'; -import 'package:path/path.dart' as p; -import 'package:tuple/tuple.dart'; -import 'ast/sass.dart'; -import 'callable.dart'; -import 'compile.dart'; -import 'compile_result.dart'; -import 'exception.dart'; -import 'io.dart'; -import 'importer/node.dart'; import 'node/exports.dart'; -import 'node/function.dart'; -import 'node/render_context.dart'; -import 'node/render_options.dart'; -import 'node/render_result.dart'; -import 'node/types.dart'; -import 'node/value.dart'; +import 'node/legacy.dart'; +import 'node/legacy/types.dart'; +import 'node/legacy/value.dart'; import 'node/utils.dart'; -import 'parse/scss.dart'; -import 'syntax.dart'; -import 'util/nullable.dart'; import 'value.dart'; -import 'visitor/serialize.dart'; /// The entrypoint for the Node.js module. /// /// This sets up exports that can be called from JS. void main() { - exports.render = allowInterop(_render); - exports.renderSync = allowInterop(_renderSync); + exports.render = allowInterop(render); + exports.renderSync = allowInterop(renderSync); exports.info = "dart-sass\t${const String.fromEnvironment('version')}\t(Sass Compiler)\t" "[Dart]\n" @@ -58,391 +36,3 @@ void main() { exports.TRUE = sassTrue; exports.FALSE = sassFalse; } - -/// Converts Sass to CSS. -/// -/// This attempts to match the [node-sass `render()` API][render] as closely as -/// possible. -/// -/// [render]: https://github.com/sass/node-sass#options -void _render( - RenderOptions options, void callback(Object? error, RenderResult? result)) { - var fiber = options.fiber; - if (fiber != null) { - fiber.call(allowInterop(() { - try { - callback(null, _renderSync(options)); - } catch (error) { - callback(error, null); - } - return null; - })).run(); - } else { - _renderAsync(options).then((result) { - callback(null, result); - }, onError: (Object error, StackTrace stackTrace) { - if (error is SassException) { - callback(_wrapException(error), null); - } else { - callback(_newRenderError(error.toString(), status: 3), null); - } - }); - } -} - -/// Converts Sass to CSS asynchronously. -Future _renderAsync(RenderOptions options) async { - var start = DateTime.now(); - CompileResult result; - - var data = options.data; - var file = options.file.andThen(p.absolute); - if (data != null) { - result = await compileStringAsync(data, - nodeImporter: _parseImporter(options, start), - functions: _parseFunctions(options, start, asynch: true), - syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, - style: _parseOutputStyle(options.outputStyle), - useSpaces: options.indentType != 'tab', - indentWidth: _parseIndentWidth(options.indentWidth), - lineFeed: _parseLineFeed(options.linefeed), - url: file == null ? 'stdin' : p.toUri(file).toString(), - quietDeps: options.quietDeps ?? false, - verbose: options.verbose ?? false, - charset: options.charset ?? true, - sourceMap: _enableSourceMaps(options)); - } else if (file != null) { - result = await compileAsync(file, - nodeImporter: _parseImporter(options, start), - functions: _parseFunctions(options, start, asynch: true), - syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, - style: _parseOutputStyle(options.outputStyle), - useSpaces: options.indentType != 'tab', - indentWidth: _parseIndentWidth(options.indentWidth), - lineFeed: _parseLineFeed(options.linefeed), - quietDeps: options.quietDeps ?? false, - verbose: options.verbose ?? false, - charset: options.charset ?? true, - sourceMap: _enableSourceMaps(options)); - } else { - throw ArgumentError("Either options.data or options.file must be set."); - } - - return _newRenderResult(options, result, start); -} - -/// Converts Sass to CSS. -/// -/// This attempts to match the [node-sass `renderSync()` API][render] as closely -/// as possible. -/// -/// [render]: https://github.com/sass/node-sass#options -RenderResult _renderSync(RenderOptions options) { - try { - var start = DateTime.now(); - CompileResult result; - - var data = options.data; - var file = options.file.andThen(p.absolute); - if (data != null) { - result = compileString(data, - nodeImporter: _parseImporter(options, start), - functions: _parseFunctions(options, start).cast(), - syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, - style: _parseOutputStyle(options.outputStyle), - useSpaces: options.indentType != 'tab', - indentWidth: _parseIndentWidth(options.indentWidth), - lineFeed: _parseLineFeed(options.linefeed), - url: file == null ? 'stdin' : p.toUri(file).toString(), - quietDeps: options.quietDeps ?? false, - verbose: options.verbose ?? false, - charset: options.charset ?? true, - sourceMap: _enableSourceMaps(options)); - } else if (file != null) { - result = compile(file, - nodeImporter: _parseImporter(options, start), - functions: _parseFunctions(options, start).cast(), - syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, - style: _parseOutputStyle(options.outputStyle), - useSpaces: options.indentType != 'tab', - indentWidth: _parseIndentWidth(options.indentWidth), - lineFeed: _parseLineFeed(options.linefeed), - quietDeps: options.quietDeps ?? false, - verbose: options.verbose ?? false, - charset: options.charset ?? true, - sourceMap: _enableSourceMaps(options)); - } else { - throw ArgumentError("Either options.data or options.file must be set."); - } - - return _newRenderResult(options, result, start); - } on SassException catch (error) { - jsThrow(_wrapException(error)); - } catch (error) { - jsThrow(_newRenderError(error.toString(), status: 3)); - } - throw "unreachable"; -} - -/// Converts an exception to a [JsError]. -JsError _wrapException(Object exception) { - if (exception is SassException) { - String file; - var url = exception.span.sourceUrl; - if (url == null) { - file = 'stdin'; - } else if (url.scheme == 'file') { - file = p.fromUri(url); - } else { - file = url.toString(); - } - - return _newRenderError(exception.toString().replaceFirst("Error: ", ""), - line: exception.span.start.line + 1, - column: exception.span.start.column + 1, - file: file, - status: 1); - } else { - return JsError(exception.toString()); - } -} - -/// Parses `functions` from [RenderOptions] into a list of [Callable]s or -/// [AsyncCallable]s. -/// -/// This is typed to always return [AsyncCallable], but in practice it will -/// return a `List` if [asynch] is `false`. -List _parseFunctions(RenderOptions options, DateTime start, - {bool asynch = false}) { - var functions = options.functions; - if (functions == null) return const []; - - var result = []; - jsForEach(functions, (signature, callback) { - Tuple2 tuple; - try { - tuple = ScssParser(signature as String).parseSignature(); - } on SassFormatException catch (error) { - throw SassFormatException( - 'Invalid signature "$signature": ${error.message}', error.span); - } - - var context = RenderContext(options: _contextOptions(options, start)); - context.options.context = context; - - var fiber = options.fiber; - if (fiber != null) { - result.add(BuiltInCallable.parsed(tuple.item1, tuple.item2, (arguments) { - var currentFiber = fiber.current; - var jsArguments = [ - ...arguments.map(wrapValue), - allowInterop(([Object? result]) { - // Schedule a microtask so we don't try to resume the running fiber - // if [importer] calls `done()` synchronously. - scheduleMicrotask(() => currentFiber.run(result)); - }) - ]; - var result = (callback as JSFunction).apply(context, jsArguments); - return unwrapValue(isUndefined(result) - // Run `fiber.yield()` in runZoned() so that Dart resets the current - // zone once it's done. Otherwise, interweaving fibers can leave - // `Zone.current` in an inconsistent state. - ? runZoned(() => fiber.yield()) - : result); - })); - } else if (!asynch) { - result.add(BuiltInCallable.parsed( - tuple.item1, - tuple.item2, - (arguments) => unwrapValue((callback as JSFunction) - .apply(context, arguments.map(wrapValue).toList())))); - } else { - result.add(AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2, - (arguments) async { - var completer = Completer(); - var jsArguments = [ - ...arguments.map(wrapValue), - allowInterop(([Object? result]) => completer.complete(result)) - ]; - var result = (callback as JSFunction).apply(context, jsArguments); - return unwrapValue( - isUndefined(result) ? await completer.future : result); - })); - } - }); - return result; -} - -/// Parses [importer] and [includePaths] from [RenderOptions] into a -/// [NodeImporter]. -NodeImporter _parseImporter(RenderOptions options, DateTime start) { - List importers; - if (options.importer == null) { - importers = []; - } else if (options.importer is List) { - importers = (options.importer as List).cast(); - } else { - importers = [options.importer as JSFunction]; - } - - var contextOptions = - importers.isNotEmpty ? _contextOptions(options, start) : Object(); - - var fiber = options.fiber; - if (fiber != null) { - importers = importers.map((importer) { - return allowInteropCaptureThis( - (Object thisArg, String url, String previous, [Object? _]) { - var currentFiber = fiber.current; - var result = call3(importer, thisArg, url, previous, - allowInterop((Object result) { - // Schedule a microtask so we don't try to resume the running fiber if - // [importer] calls `done()` synchronously. - scheduleMicrotask(() => currentFiber.run(result)); - })); - - // Run `fiber.yield()` in runZoned() so that Dart resets the current - // zone once it's done. Otherwise, interweaving fibers can leave - // `Zone.current` in an inconsistent state. - if (isUndefined(result)) return runZoned(() => fiber.yield()); - return result; - }) as JSFunction; - }).toList(); - } - - var includePaths = List.from(options.includePaths ?? []); - return NodeImporter(contextOptions, includePaths, importers); -} - -/// Creates the [RenderContextOptions] for the `this` context in which custom -/// functions and importers will be evaluated. -RenderContextOptions _contextOptions(RenderOptions options, DateTime start) { - var includePaths = List.from(options.includePaths ?? []); - return RenderContextOptions( - file: options.file, - data: options.data, - includePaths: ([p.current, ...includePaths]).join(isWindows ? ';' : ':'), - precision: SassNumber.precision, - style: 1, - indentType: options.indentType == 'tab' ? 1 : 0, - indentWidth: _parseIndentWidth(options.indentWidth) ?? 2, - linefeed: _parseLineFeed(options.linefeed).text, - result: RenderContextResult( - stats: RenderContextResultStats( - start: start.millisecondsSinceEpoch, - entry: options.file ?? 'data'))); -} - -/// Parse [style] into an [OutputStyle]. -OutputStyle _parseOutputStyle(String? style) { - if (style == null || style == 'expanded') return OutputStyle.expanded; - if (style == 'compressed') return OutputStyle.compressed; - throw ArgumentError('Unsupported output style "$style".'); -} - -/// Parses the indentation width into an [int]. -int? _parseIndentWidth(Object? width) { - if (width == null) return null; - return width is int ? width : int.parse(width.toString()); -} - -/// Parses the name of a line feed type into a [LineFeed]. -LineFeed _parseLineFeed(String? str) { - switch (str) { - case 'cr': - return LineFeed.cr; - case 'crlf': - return LineFeed.crlf; - case 'lfcr': - return LineFeed.lfcr; - default: - return LineFeed.lf; - } -} - -/// Creates a [RenderResult] that exposes [result] in the Node Sass API format. -RenderResult _newRenderResult( - RenderOptions options, CompileResult result, DateTime start) { - var end = DateTime.now(); - - var css = result.css; - Uint8List? sourceMapBytes; - if (_enableSourceMaps(options)) { - var sourceMapOption = options.sourceMap; - var sourceMapPath = - sourceMapOption is String ? sourceMapOption : options.outFile! + '.map'; - var sourceMapDir = p.dirname(sourceMapPath); - - var sourceMap = result.sourceMap!; - sourceMap.sourceRoot = options.sourceMapRoot; - var outFile = options.outFile; - if (outFile == null) { - var file = options.file; - if (file == null) { - sourceMap.targetUrl = 'stdin.css'; - } else { - sourceMap.targetUrl = p.toUri(p.setExtension(file, '.css')).toString(); - } - } else { - sourceMap.targetUrl = - p.toUri(p.relative(outFile, from: sourceMapDir)).toString(); - } - - var sourceMapDirUrl = p.toUri(sourceMapDir).toString(); - for (var i = 0; i < sourceMap.urls.length; i++) { - var source = sourceMap.urls[i]; - if (source == "stdin") continue; - - // URLs handled by Node importers that directly return file contents are - // preserved in their original (usually relative) form. They may or may - // not be intended as `file:` URLs, but there's nothing we can do about it - // either way so we keep them as-is. - if (p.url.isRelative(source) || p.url.isRootRelative(source)) continue; - sourceMap.urls[i] = p.url.relative(source, from: sourceMapDirUrl); - } - - var json = sourceMap.toJson( - includeSourceContents: isTruthy(options.sourceMapContents)); - sourceMapBytes = utf8Encode(jsonEncode(json)); - - if (!isTruthy(options.omitSourceMapUrl)) { - var url = isTruthy(options.sourceMapEmbed) - ? Uri.dataFromBytes(sourceMapBytes, mimeType: "application/json") - : p.toUri(outFile == null - ? sourceMapPath - : p.relative(sourceMapPath, from: p.dirname(outFile))); - css += "\n\n/*# sourceMappingURL=$url */"; - } - } - - return RenderResult( - css: utf8Encode(css), - map: sourceMapBytes, - stats: RenderResultStats( - entry: options.file ?? 'data', - start: start.millisecondsSinceEpoch, - end: end.millisecondsSinceEpoch, - duration: end.difference(start).inMilliseconds, - includedFiles: [ - for (var url in result.loadedUrls) - if (url.scheme == 'file') p.fromUri(url) else url.toString() - ])); -} - -/// Returns whether source maps are enabled by [options]. -bool _enableSourceMaps(RenderOptions options) => - options.sourceMap is String || - (isTruthy(options.sourceMap) && options.outFile != null); - -/// Creates a [JsError] with the given fields added to it so it acts like a Node -/// Sass error. -JsError _newRenderError(String message, - {int? line, int? column, String? file, int? status}) { - var error = JsError(message); - setProperty(error, 'formatted', 'Error: $message'); - if (line != null) setProperty(error, 'line', line); - if (column != null) setProperty(error, 'column', column); - if (file != null) setProperty(error, 'file', file); - if (status != null) setProperty(error, 'status', status); - return error; -} diff --git a/lib/src/node/exports.dart b/lib/src/node/exports.dart index db7739969..2162f7d2f 100644 --- a/lib/src/node/exports.dart +++ b/lib/src/node/exports.dart @@ -5,7 +5,7 @@ import 'package:js/js.dart'; import '../value.dart'; -import 'types.dart'; +import 'legacy/types.dart'; @JS() class Exports { diff --git a/lib/src/node/legacy.dart b/lib/src/node/legacy.dart new file mode 100644 index 000000000..13c14160b --- /dev/null +++ b/lib/src/node/legacy.dart @@ -0,0 +1,420 @@ +// Copyright 2021 Google Inc. Use of this source code is governed by an +// MIT-style license that can be found in the LICENSE file or at +// https://opensource.org/licenses/MIT. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:js_util'; +import 'dart:typed_data'; + +import 'package:js/js.dart'; +import 'package:node_interop/js.dart'; +import 'package:path/path.dart' as p; +import 'package:tuple/tuple.dart'; + +import '../ast/sass.dart'; +import '../callable.dart'; +import '../compile.dart'; +import '../compile_result.dart'; +import '../exception.dart'; +import '../importer/legacy_node.dart'; +import '../io.dart'; +import '../parse/scss.dart'; +import '../syntax.dart'; +import '../util/nullable.dart'; +import '../value.dart'; +import '../visitor/serialize.dart'; +import 'function.dart'; +import 'legacy/render_context.dart'; +import 'legacy/render_options.dart'; +import 'legacy/render_result.dart'; +import 'legacy/value.dart'; +import 'utils.dart'; + +/// Converts Sass to CSS. +/// +/// This attempts to match the [node-sass `render()` API][render] as closely as +/// possible. +/// +/// [render]: https://github.com/sass/node-sass#options +void render( + RenderOptions options, void callback(Object? error, RenderResult? result)) { + var fiber = options.fiber; + if (fiber != null) { + fiber.call(allowInterop(() { + try { + callback(null, renderSync(options)); + } catch (error) { + callback(error, null); + } + return null; + })).run(); + } else { + _renderAsync(options).then((result) { + callback(null, result); + }, onError: (Object error, StackTrace stackTrace) { + if (error is SassException) { + callback(_wrapException(error), null); + } else { + callback(_newRenderError(error.toString(), status: 3), null); + } + }); + } +} + +/// Converts Sass to CSS asynchronously. +Future _renderAsync(RenderOptions options) async { + var start = DateTime.now(); + CompileResult result; + + var data = options.data; + var file = options.file.andThen(p.absolute); + if (data != null) { + result = await compileStringAsync(data, + nodeImporter: _parseImporter(options, start), + functions: _parseFunctions(options, start, asynch: true), + syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, + style: _parseOutputStyle(options.outputStyle), + useSpaces: options.indentType != 'tab', + indentWidth: _parseIndentWidth(options.indentWidth), + lineFeed: _parseLineFeed(options.linefeed), + url: file == null ? 'stdin' : p.toUri(file).toString(), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, + charset: options.charset ?? true, + sourceMap: _enableSourceMaps(options)); + } else if (file != null) { + result = await compileAsync(file, + nodeImporter: _parseImporter(options, start), + functions: _parseFunctions(options, start, asynch: true), + syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, + style: _parseOutputStyle(options.outputStyle), + useSpaces: options.indentType != 'tab', + indentWidth: _parseIndentWidth(options.indentWidth), + lineFeed: _parseLineFeed(options.linefeed), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, + charset: options.charset ?? true, + sourceMap: _enableSourceMaps(options)); + } else { + throw ArgumentError("Either options.data or options.file must be set."); + } + + return _newRenderResult(options, result, start); +} + +/// Converts Sass to CSS. +/// +/// This attempts to match the [node-sass `renderSync()` API][render] as closely +/// as possible. +/// +/// [render]: https://github.com/sass/node-sass#options +RenderResult renderSync(RenderOptions options) { + try { + var start = DateTime.now(); + CompileResult result; + + var data = options.data; + var file = options.file.andThen(p.absolute); + if (data != null) { + result = compileString(data, + nodeImporter: _parseImporter(options, start), + functions: _parseFunctions(options, start).cast(), + syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, + style: _parseOutputStyle(options.outputStyle), + useSpaces: options.indentType != 'tab', + indentWidth: _parseIndentWidth(options.indentWidth), + lineFeed: _parseLineFeed(options.linefeed), + url: file == null ? 'stdin' : p.toUri(file).toString(), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, + charset: options.charset ?? true, + sourceMap: _enableSourceMaps(options)); + } else if (file != null) { + result = compile(file, + nodeImporter: _parseImporter(options, start), + functions: _parseFunctions(options, start).cast(), + syntax: isTruthy(options.indentedSyntax) ? Syntax.sass : null, + style: _parseOutputStyle(options.outputStyle), + useSpaces: options.indentType != 'tab', + indentWidth: _parseIndentWidth(options.indentWidth), + lineFeed: _parseLineFeed(options.linefeed), + quietDeps: options.quietDeps ?? false, + verbose: options.verbose ?? false, + charset: options.charset ?? true, + sourceMap: _enableSourceMaps(options)); + } else { + throw ArgumentError("Either options.data or options.file must be set."); + } + + return _newRenderResult(options, result, start); + } on SassException catch (error) { + jsThrow(_wrapException(error)); + } catch (error) { + jsThrow(_newRenderError(error.toString(), status: 3)); + } + throw "unreachable"; +} + +/// Converts an exception to a [JsError]. +JsError _wrapException(Object exception) { + if (exception is SassException) { + String file; + var url = exception.span.sourceUrl; + if (url == null) { + file = 'stdin'; + } else if (url.scheme == 'file') { + file = p.fromUri(url); + } else { + file = url.toString(); + } + + return _newRenderError(exception.toString().replaceFirst("Error: ", ""), + line: exception.span.start.line + 1, + column: exception.span.start.column + 1, + file: file, + status: 1); + } else { + return JsError(exception.toString()); + } +} + +/// Parses `functions` from [RenderOptions] into a list of [Callable]s or +/// [AsyncCallable]s. +/// +/// This is typed to always return [AsyncCallable], but in practice it will +/// return a `List` if [asynch] is `false`. +List _parseFunctions(RenderOptions options, DateTime start, + {bool asynch = false}) { + var functions = options.functions; + if (functions == null) return const []; + + var result = []; + jsForEach(functions, (signature, callback) { + Tuple2 tuple; + try { + tuple = ScssParser(signature as String).parseSignature(); + } on SassFormatException catch (error) { + throw SassFormatException( + 'Invalid signature "$signature": ${error.message}', error.span); + } + + var context = RenderContext(options: _contextOptions(options, start)); + context.options.context = context; + + var fiber = options.fiber; + if (fiber != null) { + result.add(BuiltInCallable.parsed(tuple.item1, tuple.item2, (arguments) { + var currentFiber = fiber.current; + var jsArguments = [ + ...arguments.map(wrapValue), + allowInterop(([Object? result]) { + // Schedule a microtask so we don't try to resume the running fiber + // if [importer] calls `done()` synchronously. + scheduleMicrotask(() => currentFiber.run(result)); + }) + ]; + var result = (callback as JSFunction).apply(context, jsArguments); + return unwrapValue(isUndefined(result) + // Run `fiber.yield()` in runZoned() so that Dart resets the current + // zone once it's done. Otherwise, interweaving fibers can leave + // `Zone.current` in an inconsistent state. + ? runZoned(() => fiber.yield()) + : result); + })); + } else if (!asynch) { + result.add(BuiltInCallable.parsed( + tuple.item1, + tuple.item2, + (arguments) => unwrapValue((callback as JSFunction) + .apply(context, arguments.map(wrapValue).toList())))); + } else { + result.add(AsyncBuiltInCallable.parsed(tuple.item1, tuple.item2, + (arguments) async { + var completer = Completer(); + var jsArguments = [ + ...arguments.map(wrapValue), + allowInterop(([Object? result]) => completer.complete(result)) + ]; + var result = (callback as JSFunction).apply(context, jsArguments); + return unwrapValue( + isUndefined(result) ? await completer.future : result); + })); + } + }); + return result; +} + +/// Parses [importer] and [includePaths] from [RenderOptions] into a +/// [NodeImporter]. +NodeImporter _parseImporter(RenderOptions options, DateTime start) { + List importers; + if (options.importer == null) { + importers = []; + } else if (options.importer is List) { + importers = (options.importer as List).cast(); + } else { + importers = [options.importer as JSFunction]; + } + + var contextOptions = + importers.isNotEmpty ? _contextOptions(options, start) : Object(); + + var fiber = options.fiber; + if (fiber != null) { + importers = importers.map((importer) { + return allowInteropCaptureThis( + (Object thisArg, String url, String previous, [Object? _]) { + var currentFiber = fiber.current; + var result = call3(importer, thisArg, url, previous, + allowInterop((Object result) { + // Schedule a microtask so we don't try to resume the running fiber if + // [importer] calls `done()` synchronously. + scheduleMicrotask(() => currentFiber.run(result)); + })); + + // Run `fiber.yield()` in runZoned() so that Dart resets the current + // zone once it's done. Otherwise, interweaving fibers can leave + // `Zone.current` in an inconsistent state. + if (isUndefined(result)) return runZoned(() => fiber.yield()); + return result; + }) as JSFunction; + }).toList(); + } + + var includePaths = List.from(options.includePaths ?? []); + return NodeImporter(contextOptions, includePaths, importers); +} + +/// Creates the [RenderContextOptions] for the `this` context in which custom +/// functions and importers will be evaluated. +RenderContextOptions _contextOptions(RenderOptions options, DateTime start) { + var includePaths = List.from(options.includePaths ?? []); + return RenderContextOptions( + file: options.file, + data: options.data, + includePaths: ([p.current, ...includePaths]).join(isWindows ? ';' : ':'), + precision: SassNumber.precision, + style: 1, + indentType: options.indentType == 'tab' ? 1 : 0, + indentWidth: _parseIndentWidth(options.indentWidth) ?? 2, + linefeed: _parseLineFeed(options.linefeed).text, + result: RenderContextResult( + stats: RenderContextResultStats( + start: start.millisecondsSinceEpoch, + entry: options.file ?? 'data'))); +} + +/// Parse [style] into an [OutputStyle]. +OutputStyle _parseOutputStyle(String? style) { + if (style == null || style == 'expanded') return OutputStyle.expanded; + if (style == 'compressed') return OutputStyle.compressed; + throw ArgumentError('Unsupported output style "$style".'); +} + +/// Parses the indentation width into an [int]. +int? _parseIndentWidth(Object? width) { + if (width == null) return null; + return width is int ? width : int.parse(width.toString()); +} + +/// Parses the name of a line feed type into a [LineFeed]. +LineFeed _parseLineFeed(String? str) { + switch (str) { + case 'cr': + return LineFeed.cr; + case 'crlf': + return LineFeed.crlf; + case 'lfcr': + return LineFeed.lfcr; + default: + return LineFeed.lf; + } +} + +/// Creates a [RenderResult] that exposes [result] in the Node Sass API format. +RenderResult _newRenderResult( + RenderOptions options, CompileResult result, DateTime start) { + var end = DateTime.now(); + + var css = result.css; + Uint8List? sourceMapBytes; + if (_enableSourceMaps(options)) { + var sourceMapOption = options.sourceMap; + var sourceMapPath = + sourceMapOption is String ? sourceMapOption : options.outFile! + '.map'; + var sourceMapDir = p.dirname(sourceMapPath); + + var sourceMap = result.sourceMap!; + sourceMap.sourceRoot = options.sourceMapRoot; + var outFile = options.outFile; + if (outFile == null) { + var file = options.file; + if (file == null) { + sourceMap.targetUrl = 'stdin.css'; + } else { + sourceMap.targetUrl = p.toUri(p.setExtension(file, '.css')).toString(); + } + } else { + sourceMap.targetUrl = + p.toUri(p.relative(outFile, from: sourceMapDir)).toString(); + } + + var sourceMapDirUrl = p.toUri(sourceMapDir).toString(); + for (var i = 0; i < sourceMap.urls.length; i++) { + var source = sourceMap.urls[i]; + if (source == "stdin") continue; + + // URLs handled by Node importers that directly return file contents are + // preserved in their original (usually relative) form. They may or may + // not be intended as `file:` URLs, but there's nothing we can do about it + // either way so we keep them as-is. + if (p.url.isRelative(source) || p.url.isRootRelative(source)) continue; + sourceMap.urls[i] = p.url.relative(source, from: sourceMapDirUrl); + } + + var json = sourceMap.toJson( + includeSourceContents: isTruthy(options.sourceMapContents)); + sourceMapBytes = utf8Encode(jsonEncode(json)); + + if (!isTruthy(options.omitSourceMapUrl)) { + var url = isTruthy(options.sourceMapEmbed) + ? Uri.dataFromBytes(sourceMapBytes, mimeType: "application/json") + : p.toUri(outFile == null + ? sourceMapPath + : p.relative(sourceMapPath, from: p.dirname(outFile))); + css += "\n\n/*# sourceMappingURL=$url */"; + } + } + + return RenderResult( + css: utf8Encode(css), + map: sourceMapBytes, + stats: RenderResultStats( + entry: options.file ?? 'data', + start: start.millisecondsSinceEpoch, + end: end.millisecondsSinceEpoch, + duration: end.difference(start).inMilliseconds, + includedFiles: [ + for (var url in result.loadedUrls) + if (url.scheme == 'file') p.fromUri(url) else url.toString() + ])); +} + +/// Returns whether source maps are enabled by [options]. +bool _enableSourceMaps(RenderOptions options) => + options.sourceMap is String || + (isTruthy(options.sourceMap) && options.outFile != null); + +/// Creates a [JsError] with the given fields added to it so it acts like a Node +/// Sass error. +JsError _newRenderError(String message, + {int? line, int? column, String? file, int? status}) { + var error = JsError(message); + setProperty(error, 'formatted', 'Error: $message'); + if (line != null) setProperty(error, 'line', line); + if (column != null) setProperty(error, 'column', column); + if (file != null) setProperty(error, 'file', file); + if (status != null) setProperty(error, 'status', status); + return error; +} diff --git a/lib/src/node/fiber.dart b/lib/src/node/legacy/fiber.dart similarity index 100% rename from lib/src/node/fiber.dart rename to lib/src/node/legacy/fiber.dart diff --git a/lib/src/node/importer_result.dart b/lib/src/node/legacy/importer_result.dart similarity index 100% rename from lib/src/node/importer_result.dart rename to lib/src/node/legacy/importer_result.dart diff --git a/lib/src/node/render_context.dart b/lib/src/node/legacy/render_context.dart similarity index 100% rename from lib/src/node/render_context.dart rename to lib/src/node/legacy/render_context.dart diff --git a/lib/src/node/render_options.dart b/lib/src/node/legacy/render_options.dart similarity index 100% rename from lib/src/node/render_options.dart rename to lib/src/node/legacy/render_options.dart diff --git a/lib/src/node/render_result.dart b/lib/src/node/legacy/render_result.dart similarity index 100% rename from lib/src/node/render_result.dart rename to lib/src/node/legacy/render_result.dart diff --git a/lib/src/node/types.dart b/lib/src/node/legacy/types.dart similarity index 100% rename from lib/src/node/types.dart rename to lib/src/node/legacy/types.dart diff --git a/lib/src/node/value.dart b/lib/src/node/legacy/value.dart similarity index 96% rename from lib/src/node/value.dart rename to lib/src/node/legacy/value.dart index 38f9bc322..d69f961b8 100644 --- a/lib/src/node/value.dart +++ b/lib/src/node/legacy/value.dart @@ -4,8 +4,8 @@ import 'dart:js_util'; -import '../value.dart'; -import 'utils.dart'; +import '../../value.dart'; +import '../utils.dart'; import 'value/color.dart'; import 'value/list.dart'; import 'value/map.dart'; diff --git a/lib/src/node/value/boolean.dart b/lib/src/node/legacy/value/boolean.dart similarity index 95% rename from lib/src/node/value/boolean.dart rename to lib/src/node/legacy/value/boolean.dart index e547ef0e2..2d3048241 100644 --- a/lib/src/node/value/boolean.dart +++ b/lib/src/node/legacy/value/boolean.dart @@ -6,8 +6,8 @@ import 'dart:js_util'; import 'package:js/js.dart'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; /// The JS constructor for the `sass.types.Boolean` class. /// diff --git a/lib/src/node/value/color.dart b/lib/src/node/legacy/value/color.dart similarity index 97% rename from lib/src/node/value/color.dart rename to lib/src/node/legacy/value/color.dart index 859d99f1b..e511f2b44 100644 --- a/lib/src/node/value/color.dart +++ b/lib/src/node/legacy/value/color.dart @@ -6,8 +6,8 @@ import 'dart:js_util'; import 'package:js/js.dart'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; @JS() class _NodeSassColor { diff --git a/lib/src/node/value/list.dart b/lib/src/node/legacy/value/list.dart similarity index 96% rename from lib/src/node/value/list.dart rename to lib/src/node/legacy/value/list.dart index aad45bc72..dd10bb3a6 100644 --- a/lib/src/node/value/list.dart +++ b/lib/src/node/legacy/value/list.dart @@ -6,8 +6,8 @@ import 'package:js/js.dart'; import 'dart:js_util'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; import '../value.dart'; @JS() diff --git a/lib/src/node/value/map.dart b/lib/src/node/legacy/value/map.dart similarity index 97% rename from lib/src/node/value/map.dart rename to lib/src/node/legacy/value/map.dart index 69b4d897e..b8c44e33c 100644 --- a/lib/src/node/value/map.dart +++ b/lib/src/node/legacy/value/map.dart @@ -6,8 +6,8 @@ import 'package:js/js.dart'; import 'dart:js_util'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; import '../value.dart'; @JS() diff --git a/lib/src/node/value/null.dart b/lib/src/node/legacy/value/null.dart similarity index 94% rename from lib/src/node/value/null.dart rename to lib/src/node/legacy/value/null.dart index 10c28d0bd..310d3847e 100644 --- a/lib/src/node/value/null.dart +++ b/lib/src/node/legacy/value/null.dart @@ -6,8 +6,8 @@ import 'package:js/js.dart'; import 'dart:js_util'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; /// The JS constructor for the `sass.types.Null` class. /// diff --git a/lib/src/node/value/number.dart b/lib/src/node/legacy/value/number.dart similarity index 97% rename from lib/src/node/value/number.dart rename to lib/src/node/legacy/value/number.dart index b967832fc..f377cf035 100644 --- a/lib/src/node/value/number.dart +++ b/lib/src/node/legacy/value/number.dart @@ -6,8 +6,8 @@ import 'dart:js_util'; import 'package:js/js.dart'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; @JS() class _NodeSassNumber { diff --git a/lib/src/node/value/string.dart b/lib/src/node/legacy/value/string.dart similarity index 95% rename from lib/src/node/value/string.dart rename to lib/src/node/legacy/value/string.dart index b79af1e16..6b3f057bc 100644 --- a/lib/src/node/value/string.dart +++ b/lib/src/node/legacy/value/string.dart @@ -6,8 +6,8 @@ import 'package:js/js.dart'; import 'dart:js_util'; -import '../../value.dart'; -import '../utils.dart'; +import '../../../value.dart'; +import '../../utils.dart'; @JS() class _NodeSassString { diff --git a/lib/src/visitor/async_evaluate.dart b/lib/src/visitor/async_evaluate.dart index 9f0a8a27a..efcdffb9d 100644 --- a/lib/src/visitor/async_evaluate.dart +++ b/lib/src/visitor/async_evaluate.dart @@ -29,7 +29,7 @@ import '../extend/extension.dart'; import '../functions.dart'; import '../functions/meta.dart' as meta; import '../importer.dart'; -import '../importer/node.dart'; +import '../importer/legacy_node.dart'; import '../io.dart'; import '../logger.dart'; import '../module.dart'; diff --git a/lib/src/visitor/evaluate.dart b/lib/src/visitor/evaluate.dart index da2d24482..f40ab3586 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: 72516268980b2e5ece8c1eb38f024f22e96d5f15 +// Checksum: 8cfca5657b5368e3c6399a2753ddda351c51a4bd // // ignore_for_file: unused_import @@ -38,7 +38,7 @@ import '../extend/extension.dart'; import '../functions.dart'; import '../functions/meta.dart' as meta; import '../importer.dart'; -import '../importer/node.dart'; +import '../importer/legacy_node.dart'; import '../io.dart'; import '../logger.dart'; import '../module.dart'; diff --git a/test/node_api/api.dart b/test/legacy_node_api/api.dart similarity index 89% rename from test/node_api/api.dart rename to test/legacy_node_api/api.dart index 4f9ed9c43..5a3c9f142 100644 --- a/test/node_api/api.dart +++ b/test/legacy_node_api/api.dart @@ -9,13 +9,13 @@ import 'package:js/js.dart'; import 'package:path/path.dart' as p; -export 'package:sass/src/node/importer_result.dart'; -export 'package:sass/src/node/render_context.dart'; -export 'package:sass/src/node/render_options.dart'; -export 'package:sass/src/node/render_result.dart'; -import 'package:sass/src/node/fiber.dart'; -import 'package:sass/src/node/render_options.dart'; -import 'package:sass/src/node/render_result.dart'; +export 'package:sass/src/node/legacy/importer_result.dart'; +export 'package:sass/src/node/legacy/render_context.dart'; +export 'package:sass/src/node/legacy/render_options.dart'; +export 'package:sass/src/node/legacy/render_result.dart'; +import 'package:sass/src/node/legacy/fiber.dart'; +import 'package:sass/src/node/legacy/render_options.dart'; +import 'package:sass/src/node/legacy/render_result.dart'; /// The Sass module. final sass = _requireSass(p.absolute("build/npm/sass.dart")); diff --git a/test/node_api/function_test.dart b/test/legacy_node_api/function_test.dart similarity index 100% rename from test/node_api/function_test.dart rename to test/legacy_node_api/function_test.dart diff --git a/test/node_api/importer_test.dart b/test/legacy_node_api/importer_test.dart similarity index 100% rename from test/node_api/importer_test.dart rename to test/legacy_node_api/importer_test.dart diff --git a/test/node_api/intercept_stdout.dart b/test/legacy_node_api/intercept_stdout.dart similarity index 100% rename from test/node_api/intercept_stdout.dart rename to test/legacy_node_api/intercept_stdout.dart diff --git a/test/node_api/source_map_test.dart b/test/legacy_node_api/source_map_test.dart similarity index 100% rename from test/node_api/source_map_test.dart rename to test/legacy_node_api/source_map_test.dart diff --git a/test/node_api/utils.dart b/test/legacy_node_api/utils.dart similarity index 100% rename from test/node_api/utils.dart rename to test/legacy_node_api/utils.dart diff --git a/test/node_api/value/boolean_test.dart b/test/legacy_node_api/value/boolean_test.dart similarity index 100% rename from test/node_api/value/boolean_test.dart rename to test/legacy_node_api/value/boolean_test.dart diff --git a/test/node_api/value/color_test.dart b/test/legacy_node_api/value/color_test.dart similarity index 100% rename from test/node_api/value/color_test.dart rename to test/legacy_node_api/value/color_test.dart diff --git a/test/node_api/value/list_test.dart b/test/legacy_node_api/value/list_test.dart similarity index 100% rename from test/node_api/value/list_test.dart rename to test/legacy_node_api/value/list_test.dart diff --git a/test/node_api/value/map_test.dart b/test/legacy_node_api/value/map_test.dart similarity index 100% rename from test/node_api/value/map_test.dart rename to test/legacy_node_api/value/map_test.dart diff --git a/test/node_api/value/null_test.dart b/test/legacy_node_api/value/null_test.dart similarity index 100% rename from test/node_api/value/null_test.dart rename to test/legacy_node_api/value/null_test.dart diff --git a/test/node_api/value/number_test.dart b/test/legacy_node_api/value/number_test.dart similarity index 100% rename from test/node_api/value/number_test.dart rename to test/legacy_node_api/value/number_test.dart diff --git a/test/node_api/value/string_test.dart b/test/legacy_node_api/value/string_test.dart similarity index 100% rename from test/node_api/value/string_test.dart rename to test/legacy_node_api/value/string_test.dart diff --git a/test/node_api/value/utils.dart b/test/legacy_node_api/value/utils.dart similarity index 100% rename from test/node_api/value/utils.dart rename to test/legacy_node_api/value/utils.dart diff --git a/test/node_api_test.dart b/test/legacy_node_api_test.dart similarity index 99% rename from test/node_api_test.dart rename to test/legacy_node_api_test.dart index 63a94de87..4a7656275 100644 --- a/test/node_api_test.dart +++ b/test/legacy_node_api_test.dart @@ -15,9 +15,9 @@ import 'package:sass/src/node/utils.dart'; import 'ensure_npm_package.dart'; import 'hybrid.dart'; -import 'node_api/api.dart'; -import 'node_api/intercept_stdout.dart'; -import 'node_api/utils.dart'; +import 'legacy_node_api/api.dart'; +import 'legacy_node_api/intercept_stdout.dart'; +import 'legacy_node_api/utils.dart'; import 'utils.dart'; void main() {