Merge branch 'feature.async'

This commit is contained in:
Natalie Weizenbaum 2017-12-02 12:52:12 -08:00
commit c1b6c117cc
35 changed files with 3157 additions and 177 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ packages
.packages
pubspec.lock
/benchmark/source
node_modules/

View File

@ -4,6 +4,7 @@ env:
# Language specs, defined in sass/sass-spec
- TASK=specs DART_CHANNEL=dev DART_VERSION=latest
- TASK=specs DART_CHANNEL=stable DART_VERSION=latest
- TASK=specs DART_CHANNEL=stable DART_VERSION=latest ASYNC=true
# Unit tests, defined in test/.
- TASK=tests DART_CHANNEL=dev DART_VERSION=latest
@ -42,7 +43,7 @@ install:
- if-node . "$HOME/.nvm/nvm.sh"
- if-node nvm install "$NODE_VERSION"
- if-node nvm use "$NODE_VERSION"
- SASS_MINIFY_JS=false if-node pub run grinder npm_package
- SASS_MINIFY_JS=false if-node pub run grinder before_test
# Download sass-spec and install its dependencies if we're running specs.
- if-specs() { if [ "$TASK" = specs ]; then "$@"; fi }
@ -69,5 +70,8 @@ script:
fi;
else
echo "${bold}Running sass-spec against $(dart --version &> /dev/stdout).$none";
(cd sass-spec; bundle exec sass-spec.rb --dart ..);
if [ "$ASYNC" = true ]; then
extra_args=--dart-args --async;
fi;
(cd sass-spec; bundle exec sass-spec.rb --dart .. $extra_args);
fi

View File

@ -3,6 +3,19 @@
* Fix a crash when `:not(...)` extends a selector that appears in
`:not(:not(...))`.
### Node JS API
* Add support for asynchronous importers to `render()` and `renderSync()`.
### Dart API
* Add `compileAsync()` and `compileStringAsync()` methods. These run
asynchronously, which allows them to take asynchronous importers (see below).
* Add an `AsyncImporter` class. This allows imports to be resolved
asynchronously in case no synchronous APIs are available. `AsyncImporter`s are
only compatible with `compileAysnc()` and `compileStringAsync()`.
## 1.0.0-beta.3
* Properly parse numbers with exponents.

View File

@ -113,7 +113,32 @@ That's it!
When installed via NPM, Dart Sass supports a JavaScript API that aims to be
compatible with [Node Sass](https://github.com/sass/node-sass#usage). Full
compatibility is a work in progress, but Dart Sass currently supports the
`render()` and `renderSync()` functions with the following options:
`render()` and `renderSync()` functions. Note however that by default,
**`renderSync()` is more than twice as fast as `render()`**, due to the overhead
of asynchronous callbacks.
To avoid this performance hit, `render()` can use the [`fibers`][fibers] package
to call asynchronous importers from the synchronous code path. To enable this,
pass the `Fiber` class to the `fiber` option:
[fibers]: https://www.npmjs.com/package/fibers
```js
var sass = require("sass");
var Fiber = require("fibers");
render({
file: "input.scss",
importer: function(url, prev, done) {
// ...
},
fiber: Fiber
}, function(err, result) {
// ...
});
```
Both `render()` and `renderSync()` support the following options:
* [`file`](https://github.com/sass/node-sass#file)
* [`data`](https://github.com/sass/node-sass#data)
@ -122,13 +147,9 @@ compatibility is a work in progress, but Dart Sass currently supports the
* [`indentType`](https://github.com/sass/node-sass#indenttype)
* [`indentWidth`](https://github.com/sass/node-sass#indentwidth)
* [`linefeed`](https://github.com/sass/node-sass#linefeed)
* [`importer`](https://github.com/sass/node-sass#importer--v200---experimental)
* Only the `"expanded"` value of
[`outputStyle`](https://github.com/sass/node-sass#outputstyle) is supported.
* [`importer`][importer option] is supported, but only for importers that return
values synchronously. The `done()` callback is currently not passed to any
importers, even when running the asynchronous `render()` function.
[importer option]: https://github.com/sass/node-sass#importer--v200---experimental
The following options are not yet supported, but are intended:

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'src/compile.dart' as c;
import 'src/exception.dart';
import 'src/importer.dart';
@ -87,6 +89,48 @@ String compileString(String source,
return result.css;
}
/// Like [compile], except it runs asynchronously.
///
/// Running asynchronously allows this to take [AsyncImporter]s rather than
/// synchronous [Importer]s. However, running asynchronously is also somewhat
/// slower, so [compile] should be preferred if possible.
Future<String> compileAsync(String path,
{bool color: false,
Iterable<AsyncImporter> importers,
Iterable<String> loadPaths,
SyncPackageResolver packageResolver}) async {
var result = await c.compileAsync(path,
color: color,
importers: importers,
loadPaths: loadPaths,
packageResolver: packageResolver);
return result.css;
}
/// Like [compileString], except it runs asynchronously.
///
/// Running asynchronously allows this to take [AsyncImporter]s rather than
/// synchronous [Importer]s. However, running asynchronously is also somewhat
/// slower, so [compileString] should be preferred if possible.
Future<String> compileStringAsync(String source,
{bool indented: false,
bool color: false,
Iterable<AsyncImporter> importers,
Iterable<String> loadPaths,
SyncPackageResolver packageResolver,
AsyncImporter importer,
url}) async {
var result = await c.compileStringAsync(source,
indented: indented,
color: color,
importers: importers,
loadPaths: loadPaths,
packageResolver: packageResolver,
importer: importer,
url: url);
return result.css;
}
/// Use [compile] instead.
@Deprecated('Will be removed in 1.0.0')
String render(String path,

View File

@ -0,0 +1,306 @@
// Copyright 2016 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 'ast/sass.dart';
import 'callable.dart';
import 'functions.dart';
import 'value.dart';
import 'utils.dart';
/// The lexical environment in which Sass is executed.
///
/// This tracks lexically-scoped information, such as variables, functions, and
/// mixins.
class AsyncEnvironment {
/// A list of variables defined at each lexical scope level.
///
/// Each scope maps the names of declared variables to their values. These
/// maps are *normalized*, meaning that they treat hyphens and underscores in
/// its keys interchangeably.
///
/// The first element is the global scope, and each successive element is
/// deeper in the tree.
final List<Map<String, Value>> _variables;
/// A map of variable names to their indices in [_variables].
///
/// This map is *normalized*, meaning that it treats hyphens and underscores
/// in its keys interchangeably.
///
/// This map is filled in as-needed, and may not be complete.
final Map<String, int> _variableIndices;
/// A list of functions defined at each lexical scope level.
///
/// Each scope maps the names of declared functions to their values. These
/// maps are *normalized*, meaning that they treat hyphens and underscores in
/// its keys interchangeably.
///
/// The first element is the global scope, and each successive element is
/// deeper in the tree.
final List<Map<String, AsyncCallable>> _functions;
/// A map of function names to their indices in [_functions].
///
/// This map is *normalized*, meaning that it treats hyphens and underscores
/// in its keys interchangeably.
///
/// This map is filled in as-needed, and may not be complete.
final Map<String, int> _functionIndices;
/// A list of mixins defined at each lexical scope level.
///
/// Each scope maps the names of declared mixins to their values. These
/// maps are *normalized*, meaning that they treat hyphens and underscores in
/// its keys interchangeably.
///
/// The first element is the global scope, and each successive element is
/// deeper in the tree.
final List<Map<String, AsyncCallable>> _mixins;
/// A map of mixin names to their indices in [_mixins].
///
/// This map is *normalized*, meaning that it treats hyphens and underscores
/// in its keys interchangeably.
///
/// This map is filled in as-needed, and may not be complete.
final Map<String, int> _mixinIndices;
/// The content block passed to the lexically-enclosing mixin, or `null` if this is not
/// in a mixin, or if no content block was passed.
List<Statement> get contentBlock => _contentBlock;
List<Statement> _contentBlock;
/// The environment in which [_contentBlock] should be executed.
AsyncEnvironment get contentEnvironment => _contentEnvironment;
AsyncEnvironment _contentEnvironment;
/// Whether the environment is lexically within a mixin.
bool get inMixin => _inMixin;
var _inMixin = false;
/// Whether the environment is currently in a semi-global scope.
///
/// A semi-global scope can assign to global variables, but it doesn't declare
/// them by default.
var _inSemiGlobalScope = false;
AsyncEnvironment()
: _variables = [normalizedMap()],
_variableIndices = normalizedMap(),
_functions = [normalizedMap()],
_functionIndices = normalizedMap(),
_mixins = [normalizedMap()],
_mixinIndices = normalizedMap() {
coreFunctions.forEach(setFunction);
}
AsyncEnvironment._(this._variables, this._functions, this._mixins,
this._contentBlock, this._contentEnvironment)
// Lazily fill in the indices rather than eagerly copying them from the
// existing environment in closure() and global() because the copying took a
// lot of time and was rarely helpful. This saves a bunch of time on Susy's
// tests.
: _variableIndices = normalizedMap(),
_functionIndices = normalizedMap(),
_mixinIndices = normalizedMap();
/// Creates a closure based on this environment.
///
/// Any scope changes in this environment will not affect the closure.
/// However, any new declarations or assignments in scopes that are visible
/// when the closure was created will be reflected.
AsyncEnvironment closure() => new AsyncEnvironment._(
_variables.toList(),
_functions.toList(),
_mixins.toList(),
_contentBlock,
_contentEnvironment);
/// Returns a new environment.
///
/// The returned environment shares this environment's global, but is
/// otherwise independent.
AsyncEnvironment global() => new AsyncEnvironment._(
[_variables.first], [_functions.first], [_mixins.first], null, null);
/// Returns the value of the variable named [name], or `null` if no such
/// variable is declared.
Value getVariable(String name) {
var index = _variableIndices[name];
if (index != null) return _variables[index][name];
index = _variableIndex(name);
if (index == null) return null;
_variableIndices[name] = index;
return _variables[index][name];
}
/// Returns whether a variable named [name] exists.
bool variableExists(String name) => getVariable(name) != null;
/// Returns whether a global variable named [name] exists.
bool globalVariableExists(String name) => _variables.first.containsKey(name);
/// Returns the index of the last map in [_variables] that has a [name] key,
/// or `null` if none exists.
int _variableIndex(String name) {
for (var i = _variables.length - 1; i >= 0; i--) {
if (_variables[i].containsKey(name)) return i;
}
return null;
}
/// Sets the variable named [name] to [value].
///
/// If [global] is `true`, this sets the variable at the top-level scope.
/// Otherwise, if the variable was already defined, it'll set it in the
/// previous scope. If it's undefined, it'll set it in the current scope.
void setVariable(String name, Value value, {bool global: false}) {
if (global || _variables.length == 1) {
// Don't set the index if there's already a variable with the given name,
// since local accesses should still return the local variable.
_variableIndices.putIfAbsent(name, () => 0);
_variables.first[name] = value;
return;
}
var index = _variableIndices.putIfAbsent(
name, () => _variableIndex(name) ?? _variables.length - 1);
if (!_inSemiGlobalScope && index == 0) {
index = _variables.length - 1;
_variableIndices[name] = index;
}
_variables[index][name] = value;
}
/// Sets the variable named [name] to [value] in the current scope.
///
/// Unlike [setVariable], this will declare the variable in the current scope
/// even if a declaration already exists in an outer scope.
void setLocalVariable(String name, Value value) {
var index = _variables.length - 1;
_variableIndices[name] = index;
_variables[index][name] = value;
}
/// Returns the value of the function named [name], or `null` if no such
/// function is declared.
AsyncCallable getFunction(String name) {
var index = _functionIndices[name];
if (index != null) return _functions[index][name];
index = _functionIndex(name);
if (index == null) return null;
_functionIndices[name] = index;
return _functions[index][name];
}
/// Returns the index of the last map in [_functions] that has a [name] key,
/// or `null` if none exists.
int _functionIndex(String name) {
for (var i = _functions.length - 1; i >= 0; i--) {
if (_functions[i].containsKey(name)) return i;
}
return null;
}
/// Returns whether a function named [name] exists.
bool functionExists(String name) => getFunction(name) != null;
/// Sets the variable named [name] to [value] in the current scope.
void setFunction(AsyncCallable callable) {
var index = _functions.length - 1;
_functionIndices[callable.name] = index;
_functions[index][callable.name] = callable;
}
/// Returns the value of the mixin named [name], or `null` if no such mixin is
/// declared.
AsyncCallable getMixin(String name) {
var index = _mixinIndices[name];
if (index != null) return _mixins[index][name];
index = _mixinIndex(name);
if (index == null) return null;
_mixinIndices[name] = index;
return _mixins[index][name];
}
/// Returns the index of the last map in [_mixins] that has a [name] key, or
/// `null` if none exists.
int _mixinIndex(String name) {
for (var i = _mixins.length - 1; i >= 0; i--) {
if (_mixins[i].containsKey(name)) return i;
}
return null;
}
/// Returns whether a mixin named [name] exists.
bool mixinExists(String name) => getMixin(name) != null;
/// Sets the variable named [name] to [value] in the current scope.
void setMixin(AsyncCallable callable) {
var index = _mixins.length - 1;
_mixinIndices[callable.name] = index;
_mixins[index][callable.name] = callable;
}
/// Sets [block] and [environment] as [contentBlock] and [contentEnvironment],
/// respectively, for the duration of [callback].
Future withContent(List<Statement> block, AsyncEnvironment environment,
Future callback()) async {
var oldBlock = _contentBlock;
var oldEnvironment = _contentEnvironment;
_contentBlock = block;
_contentEnvironment = environment;
await callback();
_contentBlock = oldBlock;
_contentEnvironment = oldEnvironment;
}
/// Sets [inMixin] to `true` for the duration of [callback].
Future asMixin(void callback()) async {
var oldInMixin = _inMixin;
_inMixin = true;
await callback();
_inMixin = oldInMixin;
}
/// Runs [callback] in a new scope.
///
/// Variables, functions, and mixins declared in a given scope are
/// inaccessible outside of it. If [semiGlobal] is passed, this scope can
/// assign to global variables without a `!global` declaration.
Future<T> scope<T>(Future<T> callback(), {bool semiGlobal: false}) async {
semiGlobal = semiGlobal && (_inSemiGlobalScope || _variables.length == 1);
// TODO: avoid creating a new scope if no variables are declared.
var wasInSemiGlobalScope = _inSemiGlobalScope;
_inSemiGlobalScope = semiGlobal;
_variables.add(normalizedMap());
_functions.add(normalizedMap());
_mixins.add(normalizedMap());
try {
return await callback();
} finally {
_inSemiGlobalScope = wasInSemiGlobalScope;
for (var name in _variables.removeLast().keys) {
_variableIndices.remove(name);
}
for (var name in _functions.removeLast().keys) {
_functionIndices.remove(name);
}
for (var name in _mixins.removeLast().keys) {
_mixinIndices.remove(name);
}
}
}
}

View File

@ -2,16 +2,22 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'callable/async.dart';
export 'callable/async.dart';
export 'callable/async_built_in.dart';
export 'callable/built_in.dart';
export 'callable/plain_css.dart';
export 'callable/user_defined.dart';
/// An interface for objects, such as functions and mixins, that can be invoked
/// from Sass by passing in arguments.
abstract class Callable {
/// The callable's name.
String get name;
///
/// This extends [AsyncCallable] because all synchronous callables are also
/// usable in asynchronous contexts. [Callable]s are usable with both the
/// synchronous and asynchronous `compile()` functions, and as such should be
/// used in preference to [AsyncCallable]s if possible.
abstract class Callable extends AsyncCallable {
// TODO(nweiz): I'd like to include the argument declaration on this interface
// as well, but supporting overloads for built-in callables makes that more
// difficult. Ideally, we'd define overloads as purely an implementation

View File

@ -0,0 +1,14 @@
// Copyright 2017 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 interface for objects, such as functions and mixins, that can be invoked
/// from Sass by passing in arguments.
///
/// This class represents callables that *need* to do asynchronous work. It's
/// only compatible with the asynchonous `compile()` methods. If a callback can
/// work synchronously, it should be a [Callable] instead.
abstract class AsyncCallable {
/// The callable's name.
String get name;
}

View File

@ -0,0 +1,62 @@
// Copyright 2017 Google Inc. Use of this source code is governed by an
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:tuple/tuple.dart';
import '../ast/sass.dart';
import '../value.dart';
import 'async.dart';
/// An [AsyncBuiltInCallable]'s callback.
typedef FutureOr<Value> _Callback(List<Value> arguments);
/// A callable defined in Dart code.
///
/// Unlike user-defined callables, built-in callables support overloads. They
/// may declare multiple different callbacks with multiple different sets of
/// arguments. When the callable is invoked, the first callback with matching
/// arguments is invoked.
class AsyncBuiltInCallable implements AsyncCallable {
final String name;
/// The overloads declared for this callable.
final _overloads = <Tuple2<ArgumentDeclaration, _Callback>>[];
/// Creates a callable with a single [arguments] declaration and a single
/// [callback].
///
/// The argument declaration is parsed from [arguments], which should not
/// include parentheses. Throws a [SassFormatException] if parsing fails.
AsyncBuiltInCallable(this.name, String arguments,
FutureOr<Value> callback(List<Value> arguments)) {
_overloads
.add(new Tuple2(new ArgumentDeclaration.parse(arguments), callback));
}
/// Creates a callable with multiple implementations.
///
/// Each key/value pair in [overloads] defines the argument declaration for
/// the overload (which should not include parentheses), and the callback to
/// execute if that argument declaration matches. Throws a
/// [SassFormatException] if parsing fails.
AsyncBuiltInCallable.overloaded(this.name, Map<String, _Callback> overloads) {
overloads.forEach((arguments, callback) {
_overloads
.add(new Tuple2(new ArgumentDeclaration.parse(arguments), callback));
});
}
/// Returns the argument declaration and Dart callback for the given
/// positional and named arguments.
///
/// Note that this doesn't guarantee that [positional] and [names] are valid
/// for the returned [ArgumentDeclaration].
Tuple2<ArgumentDeclaration, _Callback> callbackFor(
int positional, Set<String> names) =>
_overloads.take(_overloads.length - 1).firstWhere(
(overload) => overload.item1.matches(positional, names),
orElse: () => _overloads.last);
}

View File

@ -7,6 +7,7 @@ import 'package:tuple/tuple.dart';
import '../ast/sass.dart';
import '../callable.dart';
import '../value.dart';
import 'async_built_in.dart';
typedef Value _Callback(List<Value> arguments);
@ -16,7 +17,7 @@ typedef Value _Callback(List<Value> arguments);
/// may declare multiple different callbacks with multiple different sets of
/// arguments. When the callable is invoked, the first callback with matching
/// arguments is invoked.
class BuiltInCallable implements Callable {
class BuiltInCallable implements Callable, AsyncBuiltInCallable {
final String name;
/// The overloads declared for this callable.

View File

@ -4,15 +4,16 @@
import '../ast/sass.dart';
import '../callable.dart';
import '../environment.dart';
/// A callback defined in the user's Sass stylesheet.
class UserDefinedCallable implements Callable {
///
/// The type parameter [E] should either be `Environment` or `AsyncEnvironment`.
class UserDefinedCallable<E> implements Callable {
/// The declaration.
final CallableDeclaration declaration;
/// The environment in which this callable was declared.
final Environment environment;
final E environment;
String get name => declaration.name;

View File

@ -2,12 +2,15 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'ast/sass.dart';
import 'importer.dart';
import 'importer/node.dart';
import 'io.dart';
import 'sync_package_resolver.dart';
import 'util/path.dart';
import 'visitor/async_evaluate.dart';
import 'visitor/evaluate.dart';
import 'visitor/serialize.dart';
@ -57,16 +60,9 @@ CompileResult compileString(String source,
? new Stylesheet.parseSass(source, url: url, color: color)
: new Stylesheet.parseScss(source, url: url, color: color);
var importerList = (importers?.toList() ?? []);
if (loadPaths != null) {
importerList.addAll(loadPaths.map((path) => new FilesystemImporter(path)));
}
if (packageResolver != null) {
importerList.add(new PackageImporter(packageResolver));
}
var evaluateResult = evaluate(sassTree,
importers: importerList,
importers: (importers?.toList() ?? [])
..addAll(_toImporters(loadPaths, packageResolver)),
nodeImporter: nodeImporter,
importer: importer,
color: color);
@ -79,6 +75,81 @@ CompileResult compileString(String source,
return new CompileResult(css, evaluateResult.includedFiles);
}
/// Like [compileAsync] in `lib/sass.dart`, but provides more options to support
/// the node-sass compatible API.
Future<CompileResult> compileAsync(String path,
{bool indented,
bool color: false,
Iterable<AsyncImporter> importers,
NodeImporter nodeImporter,
SyncPackageResolver packageResolver,
Iterable<String> loadPaths,
OutputStyle style,
bool useSpaces: true,
int indentWidth,
LineFeed lineFeed}) =>
compileStringAsync(readFile(path),
indented: indented ?? p.extension(path) == '.sass',
color: color,
importers: importers,
nodeImporter: nodeImporter,
packageResolver: packageResolver,
loadPaths: loadPaths,
importer: new FilesystemImporter('.'),
style: style,
useSpaces: useSpaces,
indentWidth: indentWidth,
lineFeed: lineFeed,
url: p.toUri(path));
/// Like [compileStringAsync] in `lib/sass.dart`, but provides more options to
/// support the node-sass compatible API.
Future<CompileResult> compileStringAsync(String source,
{bool indented: false,
bool color: false,
Iterable<AsyncImporter> importers,
NodeImporter nodeImporter,
SyncPackageResolver packageResolver,
Iterable<String> loadPaths,
AsyncImporter importer,
OutputStyle style,
bool useSpaces: true,
int indentWidth,
LineFeed lineFeed,
url}) async {
var sassTree = indented
? new Stylesheet.parseSass(source, url: url, color: color)
: new Stylesheet.parseScss(source, url: url, color: color);
var evaluateResult = await evaluateAsync(sassTree,
importers: (importers?.toList() ?? [])
..addAll(_toImporters(loadPaths, packageResolver)),
nodeImporter: nodeImporter,
importer: importer,
color: color);
var css = serialize(evaluateResult.stylesheet,
style: style,
useSpaces: useSpaces,
indentWidth: indentWidth,
lineFeed: lineFeed);
return new CompileResult(css, evaluateResult.includedFiles);
}
/// Converts the user's [loadPaths] and [packageResolver] options into
/// importers.
List<Importer> _toImporters(
Iterable<String> loadPaths, SyncPackageResolver packageResolver) {
var list = <Importer>[];
if (loadPaths != null) {
list.addAll(loadPaths.map((path) => new FilesystemImporter(path)));
}
if (packageResolver != null) {
list.add(new PackageImporter(packageResolver));
}
return list;
}
/// The result of compiling a Sass document to CSS, along with metadata about
/// the compilation process.
class CompileResult {

View File

@ -2,6 +2,11 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// DO NOT EDIT. This file was generated from async_environment.dart.
// See tool/synchronize.dart for details.
//
// Checksum: 97410abbd78c3bbc9899f3ac460cc0736218bfe3
import 'ast/sass.dart';
import 'callable.dart';
import 'functions.dart';

View File

@ -27,7 +27,11 @@ main(List<String> args) async {
..addFlag('help',
abbr: 'h', help: 'Print this usage information.', negatable: false)
..addFlag('version',
help: 'Print the version of Dart Sass.', negatable: false);
help: 'Print the version of Dart Sass.', negatable: false)
// This is used when testing to ensure that the asynchronous evaluator path
// works the same as the synchronous one.
..addFlag('async', hide: true);
ArgResults options;
try {
@ -55,13 +59,20 @@ main(List<String> args) async {
var color =
options.wasParsed('color') ? options['color'] as bool : hasTerminal;
var asynchronous = options['async'] as bool;
try {
String css;
if (stdinFlag) {
css = await _compileStdin();
css = await _compileStdin(asynchronous: asynchronous);
} else {
var input = options.rest.first;
css = input == '-' ? await _compileStdin() : compile(input, color: color);
if (input == '-') {
css = await _compileStdin(asynchronous: asynchronous);
} else if (asynchronous) {
css = await compileAsync(input, color: color);
} else {
css = compile(input, color: color);
}
}
if (css.isNotEmpty) print(css);
@ -123,9 +134,16 @@ Future<String> _loadVersion() async {
}
/// Compiles Sass from standard input and returns the result.
Future<String> _compileStdin({bool color: false}) async =>
compileString(await readStdin(),
color: color, importer: new FilesystemImporter('.'));
Future<String> _compileStdin(
{bool asynchronous: false, bool color: false}) async {
var text = await readStdin();
var importer = new FilesystemImporter('.');
if (asynchronous) {
return await compileStringAsync(text, color: color, importer: importer);
} else {
return compileString(text, color: color, importer: importer);
}
}
/// Print the usage information for Sass, with [message] as a header.
void _printUsage(ArgParser parser, String message) {

View File

@ -2,9 +2,11 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'importer/async.dart';
import 'importer/no_op.dart';
import 'importer/result.dart';
export 'importer/async.dart';
export 'importer/filesystem.dart';
export 'importer/package.dart';
export 'importer/result.dart';
@ -16,55 +18,20 @@ export 'importer/result.dart';
/// of the importer. For example, the default filesystem importer returns its
/// load path.
///
/// This extends [AsyncImporter] to guarantee that [canonicalize] and [load] are
/// synchronous. It's usable with both the synchronous and asynchronous
/// `compile()` functions, and as such should be extended in preference to
/// [AsyncImporter] if possible.
///
/// Subclasses should extend [Importer], not implement it.
abstract class Importer {
abstract class Importer extends AsyncImporter {
/// An importer that never imports any stylesheets.
///
/// This is used for stylesheets which don't support relative imports, such as
/// those created from Dart code with plain strings.
static final Importer noOp = new NoOpImporter();
/// If [url] is recognized by this importer, returns its canonical format.
///
/// If Sass has already loaded a stylesheet with the returned canonical URL,
/// it re-uses the existing parse tree. This means that importers **must
/// ensure** that the same canonical URL always refers to the same stylesheet,
/// *even across different importers*.
///
/// This may return `null` if [url] isn't recognized by this importer.
///
/// If this importer's URL format supports file extensions, it should
/// canonicalize them the same way as the default filesystem importer:
///
/// * If the [url] ends in `.sass` or `.scss`, the importer should look for
/// a stylesheet with that exact URL and return `null` if it's not found.
///
/// * Otherwise, the importer should look for a stylesheet at `"$url.sass"` or
/// one at `"$url.scss"`, in that order. If neither is found, it should
/// return `null`.
///
/// Sass assumes that calling [canonicalize] multiple times with the same URL
/// will return the same result.
Uri canonicalize(Uri url);
/// Loads the Sass text for the given [url], or returns `null` if
/// this importer can't find the stylesheet it refers to.
///
/// The [url] comes from a call to [canonicalize] for this importer.
///
/// When Sass encounters an `@import` rule in a stylesheet, it first calls
/// [canonicalize] and [load] on the importer that first loaded that
/// stylesheet with the imported URL resolved relative to the stylesheet's
/// original URL. If either of those returns `null`, it then calls
/// [canonicalize] and [load] on each importer in order with the URL as it
/// appears in the `@import` rule.
///
/// If the importer finds a stylesheet at [url] but it fails to load for some
/// reason, or if [url] is uniquely associated with this importer but doesn't
/// refer to a real stylesheet, the importer may throw an exception that will
/// be wrapped by Sass. If the exception object has a `message` property, it
/// will be used as the wrapped exception's message; otherwise, the exception
/// object's `toString()` will be used. This means it's safe for importers to
/// throw plain strings.
ImporterResult load(Uri url);
}

View File

@ -0,0 +1,66 @@
// Copyright 2017 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 'result.dart';
/// An interface for importers that resolves URLs in `@import`s to the contents
/// of Sass files.
///
/// Importers should override [toString] to provide a human-readable description
/// of the importer. For example, the default filesystem importer returns its
/// load path.
///
/// This class should only be extended by importers that *need* to do
/// asynchronous work. It's only compatible with the asynchronous `compile()`
/// methods. If an importer can work synchronously, it should extend [Importer]
/// instead.
///
/// Subclasses should extend [AsyncImporter], not implement it.
abstract class AsyncImporter {
/// If [url] is recognized by this importer, returns its canonical format.
///
/// If Sass has already loaded a stylesheet with the returned canonical URL,
/// it re-uses the existing parse tree. This means that importers **must
/// ensure** that the same canonical URL always refers to the same stylesheet,
/// *even across different importers*.
///
/// This may return `null` if [url] isn't recognized by this importer.
///
/// If this importer's URL format supports file extensions, it should
/// canonicalize them the same way as the default filesystem importer:
///
/// * If the [url] ends in `.sass` or `.scss`, the importer should look for
/// a stylesheet with that exact URL and return `null` if it's not found.
///
/// * Otherwise, the importer should look for a stylesheet at `"$url.sass"` or
/// one at `"$url.scss"`, in that order. If neither is found, it should
/// return `null`.
///
/// Sass assumes that calling [canonicalize] multiple times with the same URL
/// will return the same result.
FutureOr<Uri> canonicalize(Uri url);
/// Loads the Sass text for the given [url], or returns `null` if
/// this importer can't find the stylesheet it refers to.
///
/// The [url] comes from a call to [canonicalize] for this importer.
///
/// When Sass encounters an `@import` rule in a stylesheet, it first calls
/// [canonicalize] and [load] on the importer that first loaded that
/// stylesheet with the imported URL resolved relative to the stylesheet's
/// original URL. If either of those returns `null`, it then calls
/// [canonicalize] and [load] on each importer in order with the URL as it
/// appears in the `@import` rule.
///
/// If the importer finds a stylesheet at [url] but it fails to load for some
/// reason, or if [url] is uniquely associated with this importer but doesn't
/// refer to a real stylesheet, the importer may throw an exception that will
/// be wrapped by Sass. If the exception object has a `message` property, it
/// will be used as the wrapped exception's message; otherwise, the exception
/// object's `toString()` will be used. This means it's safe for importers to
/// throw plain strings.
FutureOr<ImporterResult> load(Uri url);
}

View File

@ -2,6 +2,9 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:js/js.dart';
import 'package:tuple/tuple.dart';
import '../../io.dart';
@ -10,7 +13,7 @@ import '../../node/utils.dart';
import '../../util/path.dart';
import '../utils.dart';
typedef _Importer(String url, String prev);
typedef _Importer(String url, String prev, [void done(result)]);
/// An importer that encapsulates Node Sass's import logic.
///
@ -67,26 +70,30 @@ class NodeImporter {
previous.scheme == 'file' ? p.fromUri(previous) : previous.toString();
for (var importer in _importers) {
var value = call2(importer, _context, urlString, previousString);
if (value == null) continue;
if (isJSError(value)) throw value;
if (value != null) return _handleImportResult(url, previous, value);
}
NodeImporterResult result;
try {
result = value as NodeImporterResult;
} on CastError {
// is reports a different result than as here. I can't find a minimal
// reproduction, but it seems likely to be related to sdk#26838.
return null;
}
return null;
}
if (result.file != null) {
var resolved = _resolvePath(result.file, previous);
if (resolved != null) return resolved;
/// Asynchronously loads the stylesheet at [url].
///
/// The [previous] URL is the URL of the stylesheet in which the import
/// appeared. Returns the contents of the stylesheet and the URL to use as
/// [previous] for imports within the loaded stylesheet.
Future<Tuple2<String, Uri>> loadAsync(Uri url, Uri previous) async {
if (url.scheme == '' || url.scheme == 'file') {
var result = _resolvePath(p.fromUri(url), previous);
if (result != null) return result;
}
throw "Can't find stylesheet to import.";
} else {
return new Tuple2(result.contents ?? '', url);
}
// The previous URL is always an absolute file path for filesystem imports.
var urlString = url.toString();
var previousString =
previous.scheme == 'file' ? p.fromUri(previous) : previous.toString();
for (var importer in _importers) {
var value = await _callImporterAsync(importer, urlString, previousString);
if (value != null) return _handleImportResult(url, previous, value);
}
return null;
@ -129,4 +136,39 @@ class NodeImporter {
? null
: new Tuple2(readFile(resolved), p.toUri(resolved));
}
/// Converts an [_Importer]'s return [value] to a tuple that can be returned
/// by [load].
Tuple2<String, Uri> _handleImportResult(Uri url, Uri previous, Object value) {
if (isJSError(value)) throw value;
NodeImporterResult result;
try {
result = value as NodeImporterResult;
} on CastError {
// is reports a different result than as here. I can't find a minimal
// reproduction, but it seems likely to be related to sdk#26838.
return null;
}
if (result.file != null) {
var resolved = _resolvePath(result.file, previous);
if (resolved != null) return resolved;
throw "Can't find stylesheet to import.";
} else {
return new Tuple2(result.contents ?? '', url);
}
}
/// Calls an importer that may or may not be asynchronous.
Future<Object> _callImporterAsync(
_Importer importer, String urlString, String previousString) async {
var completer = new Completer();
var result = call3(importer, _context, urlString, previousString,
allowInterop(completer.complete));
if (isUndefined(result)) return await completer.future;
return result;
}
}

View File

@ -2,13 +2,17 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:tuple/tuple.dart';
typedef _Importer(String url, String prev);
typedef _Importer(String url, String prev, [void done(result)]);
class NodeImporter {
NodeImporter(Object context, Iterable<String> includePaths,
Iterable<_Importer> importers);
Tuple2<String, Uri> load(Uri url, Uri previous) => null;
Future<Tuple2<String, Uri>> loadAsync(Uri url, Uri previous) => null;
}

View File

@ -2,6 +2,8 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:js/js.dart';
@ -20,7 +22,7 @@ import 'util/path.dart';
import 'value/number.dart';
import 'visitor/serialize.dart';
typedef _Importer(String url, String prev);
typedef _Importer(String url, String prev, [void done(result)]);
/// The entrypoint for Node.js.
///
@ -46,37 +48,29 @@ void main() {
/// [render]: https://github.com/sass/node-sass#options
void _render(RenderOptions options,
void callback(RenderError error, RenderResult result)) {
try {
callback(null, _doRender(options));
} on SassException catch (error) {
callback(_wrapException(error), null);
} catch (error) {
callback(newRenderError(error.toString(), status: 3), null);
if (options.fiber != null) {
options.fiber.call(allowInterop(() {
try {
callback(null, _renderSync(options));
} catch (error) {
callback(error as RenderError, null);
}
})).run();
} else {
_renderAsync(options).then((result) {
callback(null, result);
}, onError: (error, stackTrace) {
if (error is SassException) {
callback(_wrapException(error), null);
} else {
callback(newRenderError(error.toString(), status: 3), null);
}
});
}
}
/// 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 {
return _doRender(options);
} on SassException catch (error) {
jsThrow(_wrapException(error));
} catch (error) {
jsThrow(newRenderError(error.toString(), status: 3));
}
throw "unreachable";
}
/// Converts Sass to CSS.
///
/// Unlike [_render] and [_renderSync], this doesn't do any special handling for
/// Dart exceptions.
RenderResult _doRender(RenderOptions options) {
/// Converts Sass to CSS asynchronously.
Future<RenderResult> _renderAsync(RenderOptions options) async {
var start = new DateTime.now();
CompileResult result;
if (options.data != null) {
@ -85,7 +79,7 @@ RenderResult _doRender(RenderOptions options) {
"options.data and options.file may not both be set.");
}
result = compileString(options.data,
result = await compileStringAsync(options.data,
nodeImporter: _parseImporter(options, start),
indented: options.indentedSyntax ?? false,
style: _parseOutputStyle(options.outputStyle),
@ -94,7 +88,7 @@ RenderResult _doRender(RenderOptions options) {
lineFeed: _parseLineFeed(options.linefeed),
url: 'stdin');
} else if (options.file != null) {
result = compile(options.file,
result = await compileAsync(options.file,
nodeImporter: _parseImporter(options, start),
indented: options.indentedSyntax,
style: _parseOutputStyle(options.outputStyle),
@ -114,6 +108,58 @@ RenderResult _doRender(RenderOptions options) {
includedFiles: result.includedFiles.toList());
}
/// 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 = new DateTime.now();
CompileResult result;
if (options.data != null) {
if (options.file != null) {
throw new ArgumentError(
"options.data and options.file may not both be set.");
}
result = compileString(options.data,
nodeImporter: _parseImporter(options, start),
indented: options.indentedSyntax ?? false,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed),
url: 'stdin');
} else if (options.file != null) {
result = compile(options.file,
nodeImporter: _parseImporter(options, start),
indented: options.indentedSyntax,
style: _parseOutputStyle(options.outputStyle),
useSpaces: options.indentType != 'tab',
indentWidth: _parseIndentWidth(options.indentWidth),
lineFeed: _parseLineFeed(options.linefeed));
} else {
throw new ArgumentError(
"Either options.data or options.file must be set.");
}
var end = new DateTime.now();
return newRenderResult(result.css,
entry: options.file ?? 'data',
start: start.millisecondsSinceEpoch,
end: end.millisecondsSinceEpoch,
duration: end.difference(start).inMilliseconds,
includedFiles: result.includedFiles.toList());
} on SassException catch (error) {
jsThrow(_wrapException(error));
} catch (error) {
jsThrow(newRenderError(error.toString(), status: 3));
}
throw "unreachable";
}
/// Converts a [SassException] to a [RenderError].
RenderError _wrapException(SassException exception) {
var trace = exception is SassRuntimeException
@ -170,6 +216,23 @@ NodeImporter _parseImporter(RenderOptions options, DateTime start) {
context.options.context = context;
}
if (options.fiber != null) {
importers = importers.map((importer) {
return allowInteropCaptureThis((thisArg, String url, String previous,
[_]) {
var fiber = options.fiber.current;
var result =
call3(importer, thisArg, url, previous, allowInterop((result) {
// Schedule a microtask so we don't try to resume the running fiber if
// [importer] calls `done()` synchronously.
scheduleMicrotask(() => fiber.run(result));
}));
if (isUndefined(result)) return options.fiber.yield();
return result;
}) as _Importer;
}).toList();
}
return new NodeImporter(context, includePaths, importers);
}

22
lib/src/node/fiber.dart Normal file
View File

@ -0,0 +1,22 @@
// Copyright 2017 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()
@anonymous
class FiberClass {
// Work around sdk#31490.
external Fiber call(function());
external Fiber get current;
external yield([value]);
}
@JS()
@anonymous
class Fiber {
external run([value]);
}

View File

@ -4,6 +4,8 @@
import 'package:js/js.dart';
import 'fiber.dart';
@JS()
@anonymous
class RenderOptions {
@ -16,6 +18,7 @@ class RenderOptions {
external String get indentType;
external dynamic get indentWidth;
external String get linefeed;
external FiberClass get fiber;
external factory RenderOptions(
{String file,
@ -26,5 +29,6 @@ class RenderOptions {
String outputStyle,
String indentType,
indentWidth,
String linefeed});
String linefeed,
FiberClass fiber});
}

View File

@ -23,6 +23,11 @@ void jsThrow(error) => _jsThrow.call(error);
final _jsThrow = new JSFunction("error", "throw error;");
/// Returns whether or not [value] is the JS `undefined` value.
bool isUndefined(value) => _isUndefined.call(value) as bool;
final _isUndefined = new JSFunction("value", "return value === undefined;");
@JS("Error")
external Function get _JSError;
@ -33,3 +38,8 @@ bool isJSError(value) => instanceof(value, _JSError) as bool;
R call2<R, A1, A2>(
R function(A1 arg1, A2 arg2), Object thisArg, A1 arg1, A2 arg2) =>
(function as JSFunction).apply(thisArg, [arg1, arg2]) as R;
/// Invokes [function] with [thisArg] as `this`.
R call3<R, A1, A2, A3>(R function(A1 arg1, A2 arg2, A3 arg3), Object thisArg,
A1 arg1, A2 arg2, A3 arg3) =>
(function as JSFunction).apply(thisArg, [arg1, arg2, arg3]) as R;

View File

@ -2,6 +2,7 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
import 'dart:async';
import 'dart:collection';
import 'dart:math' as math;
@ -273,6 +274,43 @@ void rotateSlice(List list, int start, int end) {
}
}
/// Like [Iterable.map] but for an asynchronous [callback].
Future<Iterable<F>> mapAsync<E, F>(
Iterable<E> iterable, Future<F> callback(E value)) async {
var result = <F>[];
for (var element in iterable) {
result.add(await callback(element));
}
return result;
}
/// Like [Map.putIfAbsent], but for an asynchronous [ifAbsent].
///
/// Note that this is *not* safe to call in parallel on the same map with the
/// same key.
Future<V> putIfAbsentAsync<K, V>(
Map<K, V> map, K key, Future<V> ifAbsent()) async {
if (map.containsKey(key)) return map[key];
var value = await ifAbsent();
map[key] = value;
return value;
}
/// Like [normalizedMapMap], but for asynchronous [key] and [value].
Future<Map<String, V2>> normalizedMapMapAsync<K, V1, V2>(Map<K, V1> map,
{Future<String> key(K key, V1 value),
Future<V2> value(K key, V1 value)}) async {
key ??= (mapKey, _) async => mapKey as String;
value ??= (_, mapValue) async => mapValue as V2;
var result = normalizedMap<V2>();
for (var mapKey in map.keys) {
var mapValue = map[mapKey];
result[await key(mapKey, mapValue)] = await value(mapKey, mapValue);
}
return result;
}
/// Prints a warning to standard error, associated with [span].
///
/// If [color] is `true`, this uses terminal colors.

View File

@ -12,7 +12,11 @@ import '../value.dart';
/// it may be passed between modules.
class SassFunction extends Value {
/// The callable that this function invokes.
final Callable callable;
///
/// Note that this is typed as an [AsyncCallback] so that it will work with
/// both synchronous and asynchronous evaluate visitors, but in practice the
/// synchronous evaluate visitor will crash if this isn't a [Callback].
final AsyncCallable callable;
SassFunction(this.callable);

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,11 @@
// MIT-style license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT.
// DO NOT EDIT. This file was generated from async_evaluate.dart.
// See tool/synchronize.dart for details.
//
// Checksum: ae64ba442752642066f0e9e038f5f2c1bbfa866b
import 'dart:math' as math;
import 'package:charcode/charcode.dart';
@ -13,9 +18,9 @@ import 'package:tuple/tuple.dart';
import '../ast/css.dart';
import '../ast/sass.dart';
import '../ast/selector.dart';
import '../environment.dart';
import '../callable.dart';
import '../color_names.dart';
import '../environment.dart';
import '../exception.dart';
import '../extend/extender.dart';
import '../importer.dart';
@ -29,7 +34,7 @@ import 'interface/statement.dart';
import 'interface/expression.dart';
/// A function that takes a callback with no arguments.
typedef _ScopeCallback(callback());
typedef void _ScopeCallback(void callback());
/// The URL used in stack traces when no source URL is available.
final _noSourceUrl = Uri.parse("-");
@ -246,8 +251,14 @@ class _EvaluateVisitor
return expression.accept(this);
}
return _runFunctionCallable(invocation,
function.assertFunction("function").callable, _callableSpan);
var callable = function.assertFunction("function").callable;
if (callable is Callable) {
return _runFunctionCallable(invocation, callable, _callableSpan);
} else {
throw new SassScriptException(
"The function ${callable.name} is asynchronous.\n"
"This is probably caused by a bug in a Sass plugin.");
}
}));
}
@ -295,12 +306,12 @@ class _EvaluateVisitor
}
Value visitAtRootRule(AtRootRule node) {
var query = node.query == null
? AtRootQuery.defaultQuery
: _adjustParseError(
node.query.span,
() => new AtRootQuery.parse(
_performInterpolation(node.query, warnForColor: true)));
var query = AtRootQuery.defaultQuery;
if (node.query != null) {
var resolved = _performInterpolation(node.query, warnForColor: true);
query = _adjustParseError(
node.query.span, () => new AtRootQuery.parse(resolved));
}
var parent = _parent;
var included = <CssParentNode>[];
@ -378,7 +389,7 @@ class _EvaluateVisitor
/// [_parent] to [newParent].
_ScopeCallback _scopeForAtRoot(CssParentNode newParent, AtRootQuery query,
List<CssParentNode> included) {
var scope = (callback()) {
var scope = (void callback()) {
// We can't use [_withParent] here because it'll add the node to the tree
// in the wrong place.
var oldParent = _parent;
@ -617,11 +628,13 @@ class _EvaluateVisitor
}
Value visitIfRule(IfRule node) {
var clause = node.clauses
.firstWhere((pair) => pair.item1.accept(this).isTruthy,
orElse: () => null)
?.item2 ??
node.lastClause;
var clause = node.lastClause;
for (var pair in node.clauses) {
if (pair.item1.accept(this).isTruthy) {
clause = pair.item2;
break;
}
}
if (clause == null) return null;
return _environment.scope(
@ -786,7 +799,8 @@ class _EvaluateVisitor
}
Value visitIncludeRule(IncludeRule node) {
var mixin = _environment.getMixin(node.name) as UserDefinedCallable;
var mixin =
_environment.getMixin(node.name) as UserDefinedCallable<Environment>;
if (mixin == null) {
throw _exception("Undefined mixin.", node.span);
}
@ -795,18 +809,16 @@ class _EvaluateVisitor
throw _exception("Mixin doesn't accept a content block.", node.span);
}
Value callback() {
_environment.asMixin(() {
for (var statement in mixin.declaration.children) {
statement.accept(this);
}
});
return null;
}
var environment = node.children == null ? null : _environment.closure();
_runUserDefinedCallable(node.arguments, mixin, node.span, () {
_environment.withContent(node.children, environment, callback);
_environment.withContent(node.children, environment, () {
_environment.asMixin(() {
for (var statement in mixin.declaration.children) {
statement.accept(this);
}
});
return null;
});
});
return null;
@ -869,11 +881,13 @@ class _EvaluateVisitor
/// Evaluates [interpolation] and parses the result as a list of media
/// queries.
List<CssMediaQuery> _visitMediaQueries(Interpolation interpolation) =>
_adjustParseError(
interpolation.span,
() => CssMediaQuery.parseList(
_performInterpolation(interpolation, warnForColor: true)));
List<CssMediaQuery> _visitMediaQueries(Interpolation interpolation) {
var resolved = _performInterpolation(interpolation, warnForColor: true);
// TODO(nweiz): Remove this type argument when sdk#31398 is fixed.
return _adjustParseError<List<CssMediaQuery>>(
interpolation.span, () => CssMediaQuery.parseList(resolved));
}
/// Returns a list of queries that selects for platforms that match both
/// [queries1] and [queries2].
@ -1169,7 +1183,7 @@ class _EvaluateVisitor
SassColor visitColorExpression(ColorExpression node) => node.value;
SassList visitListExpression(ListExpression node) => new SassList(
node.contents.map((expression) => expression.accept(this)),
node.contents.map((Expression expression) => expression.accept(this)),
node.separator,
brackets: node.hasBrackets);
@ -1202,7 +1216,7 @@ class _EvaluateVisitor
/// Evaluates the arguments in [arguments] as applied to [callable], and
/// invokes [run] in a scope with those arguments defined.
Value _runUserDefinedCallable(ArgumentInvocation arguments,
UserDefinedCallable callable, FileSpan span, Value run()) {
UserDefinedCallable<Environment> callable, FileSpan span, Value run()) {
var triple = _evaluateArguments(arguments, span);
var positional = triple.item1;
var named = triple.item2;
@ -1267,7 +1281,7 @@ class _EvaluateVisitor
ArgumentInvocation arguments, Callable callable, FileSpan span) {
if (callable is BuiltInCallable) {
return _runBuiltInCallable(arguments, callable, span).withoutSlash();
} else if (callable is UserDefinedCallable) {
} else if (callable is UserDefinedCallable<Environment>) {
return _runUserDefinedCallable(arguments, callable, span, () {
for (var statement in callable.declaration.children) {
var returnValue = statement.accept(this);
@ -1368,7 +1382,7 @@ class _EvaluateVisitor
Tuple3<List<Value>, Map<String, Value>, ListSeparator> _evaluateArguments(
ArgumentInvocation arguments, FileSpan span) {
var positional = arguments.positional
.map((expression) => expression.accept(this))
.map((Expression expression) => expression.accept(this))
.toList();
var named = normalizedMapMap<String, Expression, Value>(arguments.named,
value: (_, expression) => expression.accept(this));

9
package.json Normal file
View File

@ -0,0 +1,9 @@
{
"//": [
"This isn't the official package.json for Dart Sass. It's just used to ",
"install dependencies used for testing the Node API."
],
"devDependencies": {
"fibers": ">=1.0.0 <3.0.0"
}
}

View File

@ -25,6 +25,8 @@ dependencies:
dev_dependencies:
archive: "^1.0.0"
analyzer: "^0.30.0"
crypto: ">=0.9.2 <3.0.0"
dart_style: "^1.0.0"
grinder: "^0.8.0"
http: "^0.11.0"
@ -34,6 +36,6 @@ dev_dependencies:
stream_channel: "^1.0.0"
test_descriptor: "^1.0.0"
test_process: "^1.0.0-rc.1"
test: "^0.12.26"
test: "^0.12.29"
xml: "^2.4.0"
yaml: "^2.0.0"

View File

@ -13,23 +13,30 @@ hybridMain(StreamChannel channel) async {
throw "NPM package is not built. Run pub run grinder npm_package.";
}
var lastModified = new DateTime(0);
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList()
..add(new File("pubspec.lock"));
var lastModified = new File("build/npm/package.json").lastModifiedSync();
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList();
// If we have a dependency override, "pub run" will touch the lockfile to mark
// it as newer than the pubspec, which makes it unsuitable to use for
// freshness checking.
if (new File("pubspec.yaml")
.readAsStringSync()
.contains("dependency_overrides")) {
entriesToCheck.add(new File("pubspec.yaml"));
} else {
entriesToCheck.add(new File("pubspec.lock"));
}
for (var entry in entriesToCheck) {
if (entry is File) {
var entryLastModified = entry.lastModifiedSync();
if (lastModified.isBefore(entryLastModified)) {
lastModified = entryLastModified;
throw "${entry.path} was modified after NPM package was generated.\n"
"Run pub run grinder before_test.";
}
}
}
if (lastModified
.isAfter(new File("build/npm/package.json").lastModifiedSync())) {
throw "NPM package is out-of-date. Run pub run grinder npm_package.";
}
channel.sink.close();
}

View File

@ -6,6 +6,7 @@
/// to Dart. This is kind of convoluted, but it allows us to test the API as it
/// will be used in the real world without having to manually write any JS.
import 'package:sass/src/node/fiber.dart';
import 'package:sass/src/node/render_error.dart';
import 'package:sass/src/node/render_options.dart';
import 'package:sass/src/node/render_result.dart';
@ -20,16 +21,34 @@ export 'package:sass/src/node/render_options.dart';
export 'package:sass/src/node/render_result.dart';
/// The Sass module.
final sass = _require(p.absolute("build/npm/sass.dart"));
final sass = _requireSass(p.absolute("build/npm/sass.dart"));
/// The Fiber class.
final fiber = _requireFiber("fibers");
/// A `null` that's guaranteed to be represented by JavaScript's `undefined`
/// value, not by `null`.
@JS()
external Object get undefined;
/// A `null` that's guaranteed to be represented by JavaScript's `null` value,
/// not by `undefined`.
///
/// We have to use eval here because otherwise dart2js will inline the null
/// value and then optimize it away.
final Object jsNull = _eval("null");
@JS("eval")
external Object _eval(String js);
@JS("process.chdir")
external void chdir(String directory);
@JS("require")
external Sass _require(String path);
external Sass _requireSass(String path);
@JS("require")
external FiberClass _requireFiber(String path);
@JS()
class Sass {

View File

@ -13,6 +13,7 @@ import 'package:test/test.dart';
import 'package:sass/src/io.dart';
import 'package:sass/src/util/path.dart';
import 'package:sass/src/node/utils.dart';
import 'package:sass/src/value/number.dart';
import '../ensure_npm_package.dart';
@ -534,4 +535,121 @@ void main() {
" stdin 1:9 root stylesheet"));
});
});
group("render()", () {
test("supports asynchronous importers", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
new Future.delayed(Duration.ZERO).then((_) {
done(new NodeImporterResult(contents: 'a {b: c}'));
});
}))),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports asynchronous errors", () {
expect(
renderError(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
new Future.delayed(Duration.ZERO).then((_) {
done(new JSError('oh no'));
});
}))),
completion(toStringAndMessageEqual("oh no\n"
" stdin 1:9 root stylesheet")));
});
test("supports synchronous importers", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, ___) =>
new NodeImporterResult(contents: 'a {b: c}')))),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports synchronous null returns", () {
expect(
renderError(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, ___) => jsNull))),
completion(
toStringAndMessageEqual("Can't find stylesheet to import.\n"
" stdin 1:9 root stylesheet")));
});
group("with fibers", () {
setUpAll(() {
try {
fiber;
} catch (_) {
throw "Can't load fibers package.\n"
"Run pub run grinder before_test.";
}
});
test("supports asynchronous importers", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
new Future.delayed(Duration.ZERO).then((_) {
done(new NodeImporterResult(contents: 'a {b: c}'));
});
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports synchronous calls to done", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
done(new NodeImporterResult(contents: 'a {b: c}'));
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports synchronous importers", () {
expect(
render(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, ___) {
return new NodeImporterResult(contents: 'a {b: c}');
}),
fiber: fiber)),
completion(equalsIgnoringWhitespace('a { b: c; }')));
});
test("supports asynchronous errors", () {
expect(
renderError(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, done) {
new Future.delayed(Duration.ZERO).then((_) {
done(new JSError('oh no'));
});
}),
fiber: fiber)),
completion(toStringAndMessageEqual("oh no\n"
" stdin 1:9 root stylesheet")));
});
test("supports synchronous null returns", () {
expect(
renderError(new RenderOptions(
data: "@import 'foo'",
importer: allowInterop((_, __, ___) => jsNull),
fiber: fiber)),
completion(
toStringAndMessageEqual("Can't find stylesheet to import.\n"
" stdin 1:9 root stylesheet")));
});
});
});
}

View File

@ -37,10 +37,11 @@ Matcher toStringAndMessageEqual(String text) => predicate((error) {
/// Returns the result of rendering via [options] as a string.
Future<String> render(RenderOptions options) {
var completer = new Completer<String>();
sass.render(options, allowInterop((error, result) {
sass.render(options,
allowInterop(Zone.current.bindBinaryCallback((error, result) {
expect(error, isNull);
completer.complete(UTF8.decode(result.css));
}));
})));
return completer.future;
}
@ -48,10 +49,11 @@ Future<String> render(RenderOptions options) {
/// error.
Future<RenderError> renderError(RenderOptions options) {
var completer = new Completer<RenderError>();
sass.render(options, allowInterop((error, result) {
sass.render(options,
allowInterop(Zone.current.bindBinaryCallback((error, result) {
expect(result, isNull);
completer.complete(error);
}));
})));
return completer.future;
}

View File

@ -0,0 +1,27 @@
// Copyright 2017 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:convert';
import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:test/test.dart';
void main() {
test("synchronized files are up-to-date", () {
({
'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
'lib/src/async_environment.dart': 'lib/src/environment.dart'
}).forEach((sourcePath, targetPath) {
var source = new File(sourcePath).readAsStringSync();
var target = new File(targetPath).readAsStringSync();
var hash = sha1.convert(UTF8.encode(source));
if (!target.contains("Checksum: $hash")) {
fail("$targetPath is out-of-date.\n"
"Run pub run grinder to update it.");
}
});
});
}

View File

@ -17,6 +17,10 @@ import 'package:yaml/yaml.dart';
import 'package:sass/src/util/path.dart';
import 'synchronize.dart';
export 'synchronize.dart';
/// The version of Dart Sass.
final String _version =
loadYaml(new File('pubspec.yaml').readAsStringSync())['version'] as String;
@ -33,7 +37,11 @@ final _sdkDir = p.dirname(p.dirname(Platform.resolvedExecutable));
main(List<String> args) => grind(args);
@DefaultTask('Run the Dart formatter.')
@DefaultTask('Compile async code and reformat.')
@Depends(format, synchronize)
all() {}
@Task('Run the Dart formatter.')
format() {
Pub.run('dart_style',
script: 'format',
@ -90,6 +98,13 @@ npm_package() {
_writeNpmPackage('build/npm-old', json..addAll({"name": "dart-sass"}));
}
@Task('Installs dependencies from npm.')
npm_install() => run("npm", arguments: ["install"]);
@Task('Runs the tasks that are required for running tests.')
@Depends(npm_package, npm_install)
before_test() {}
/// Writes a Dart Sass NPM package to the directory at [destination].
///
/// The [json] will be used as the package's package.json.

208
tool/synchronize.dart Normal file
View File

@ -0,0 +1,208 @@
// Copyright 2017 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:convert';
import 'dart:io';
import 'package:analyzer/analyzer.dart';
import 'package:crypto/crypto.dart';
import 'package:analyzer/dart/ast/token.dart';
import 'package:dart_style/dart_style.dart';
import 'package:grinder/grinder.dart';
import 'package:path/path.dart' as p;
/// The files to compile to synchronous versions.
final _sources = {
'lib/src/visitor/async_evaluate.dart': 'lib/src/visitor/evaluate.dart',
'lib/src/async_environment.dart': 'lib/src/environment.dart'
};
/// This is how we support both synchronous and asynchronous compilation modes.
///
/// Both modes are necessary. Synchronous mode is faster, works in sync-only
/// contexts, and allows us to support Node Sass's renderSync() method.
/// Asynchronous mode allows users to write async importers and functions in
/// both Dart and JS.
///
/// The logic for synchronous and asynchronous mode is identical, but the async
/// code needs to await every statement and expression evaluation in case they
/// do asynchronous work. To avoid duplicating logic, we hand-write asynchronous
/// code for the evaluator and the environment and use this task to compile it
/// to a synchronous equivalent.
@Task('Compile async code to synchronous code.')
synchronize() {
_sources.forEach((source, target) {
var visitor = new _Visitor(new File(source).readAsStringSync(), source);
parseDartFile(source).accept(visitor);
var formatted = new DartFormatter().format(visitor.result);
new File(target).writeAsStringSync(formatted);
});
}
/// The visitor that traverses the asynchronous parse tree and converts it to
/// synchronous code.
///
/// To preserve the original whitespace and comments, this copies text from the
/// original source where possible. It tracks the [_position] at the end of the
/// text that's been written, and writes from that position to the new position
/// whenever text needs to be emitted.
class _Visitor extends RecursiveAstVisitor {
/// The source of the original asynchronous file.
final String _source;
/// The current position in [_source].
var _position = 0;
/// The buffer in which the text of the synchronous file is built up.
final _buffer = new StringBuffer();
/// The synchronous text.
String get result {
_buffer.write(_source.substring(_position));
_position = _source.length;
return _buffer.toString();
}
_Visitor(this._source, String path) {
var afterHeader = "\n".allMatches(_source).skip(3).first.end;
_buffer.writeln(_source.substring(0, afterHeader));
_buffer.writeln("""
// DO NOT EDIT. This file was generated from ${p.basename(path)}.
// See tool/synchronize.dart for details.
//
// Checksum: ${sha1.convert(UTF8.encode(_source))}
""");
_position = afterHeader;
}
void visitAwaitExpression(AwaitExpression node) {
_skip(node.awaitKeyword);
// Skip the space after "await" to work around dart-lang/dart_style#226.
_position++;
node.expression.accept(this);
}
void visitParenthesizedExpression(ParenthesizedExpression node) {
if (node.expression is AwaitExpression) {
_skip(node.leftParenthesis);
node.expression.accept(this);
_skip(node.rightParenthesis);
} else {
node.expression.accept(this);
}
}
void visitBlockFunctionBody(BlockFunctionBody node) {
_skip(node.keyword);
node.visitChildren(this);
}
void visitExpressionFunctionBody(ExpressionFunctionBody node) {
_skip(node.keyword);
node.visitChildren(this);
}
void visitMethodDeclaration(MethodDeclaration node) {
if (_synchronizeName(node.name.name) != node.name.name) {
// If the file defines any asynchronous versions of synchronous functions,
// remove them.
_skipNode(node);
} else {
super.visitMethodDeclaration(node);
}
}
void visitImportDirective(ImportDirective node) {
_skipNode(node);
var text = node.toString();
if (!text.contains("dart:async")) {
_buffer.write(text.replaceAll("async_", ""));
}
}
void visitMethodInvocation(MethodInvocation node) {
// Convert async utility methods to their synchronous equivalents.
if (node.target == null &&
["mapAsync", "putIfAbsentAsync"].contains(node.methodName.name)) {
_writeTo(node);
var arguments = node.argumentList.arguments;
_write(arguments.first);
_buffer.write(".${_synchronizeName(node.methodName.name)}");
if (node.typeArguments != null) _write(node.typeArguments);
_buffer.write("(");
_position = arguments[1].beginToken.offset;
for (var argument in arguments.skip(1)) {
argument.accept(this);
}
} else {
super.visitMethodInvocation(node);
}
}
void visitSimpleIdentifier(SimpleIdentifier node) {
_skip(node.token);
_buffer.write(_synchronizeName(node.name));
}
void visitTypeName(TypeName node) {
if (["Future", "FutureOr"].contains(node.name.name)) {
_skip(node.name.beginToken);
if (node.typeArguments != null) {
_skip(node.typeArguments.leftBracket);
node.typeArguments.arguments.first.accept(this);
_skip(node.typeArguments.rightBracket);
} else {
_buffer.write("void");
}
} else {
super.visitTypeName(node);
}
}
/// Writes [_source] to [_buffer] up to the beginning of [token], then puts
/// [_position] after [token] so it doesn't get written.
void _skip(Token token) {
if (token == null) return;
_buffer.write(_source.substring(_position, token.offset));
_position = token.end;
}
/// Writes [_source] to [_buffer] up to the beginning of [node], then puts
/// [_position] after [node] so it doesn't get written.
void _skipNode(AstNode node) {
if (node == null) return;
_writeTo(node);
_position = node.endToken.end;
}
/// Writes [_source] to [_buffer] up to the beginning of [node].
void _writeTo(AstNode node) {
_buffer.write(_source.substring(_position, node.beginToken.offset));
_position = node.beginToken.offset;
}
/// Writes the contents of [node] to [_buffer].
///
/// This leaves [_position] at the end of [node].
void _write(AstNode node) {
_position = node.beginToken.offset;
node.accept(this);
_buffer.write(_source.substring(_position, node.endToken.end));
_position = node.endToken.end;
}
/// Strips an "async" prefix or suffix from [name].
String _synchronizeName(String name) {
if (name.toLowerCase().startsWith('async')) {
return name.substring('async'.length);
} else if (name.toLowerCase().endsWith('async')) {
return name.substring(0, name.length - 'async'.length);
} else {
return name;
}
}
}