mirror of
https://github.com/danog/dart-sass.git
synced 2025-01-21 21:31:11 +01:00
parent
2dd7601ba6
commit
6ed530c895
@ -6,10 +6,11 @@ import 'package:js/js.dart';
|
||||
|
||||
import 'exception.dart';
|
||||
import 'executable.dart' as executable;
|
||||
import 'node/error.dart';
|
||||
import 'node/exports.dart';
|
||||
import 'node/options.dart';
|
||||
import 'node/result.dart';
|
||||
import 'node/render_error.dart';
|
||||
import 'node/render_options.dart';
|
||||
import 'node/render_result.dart';
|
||||
import 'node/utils.dart';
|
||||
import 'render.dart';
|
||||
import 'visitor/serialize.dart';
|
||||
|
||||
@ -35,22 +36,17 @@ void main() {
|
||||
/// possible.
|
||||
///
|
||||
/// [render]: https://github.com/sass/node-sass#options
|
||||
void _render(
|
||||
NodeOptions options, void callback(NodeError error, NodeResult result)) {
|
||||
void _render(RenderOptions options,
|
||||
void callback(RenderError error, RenderResult result)) {
|
||||
try {
|
||||
var indentWidthValue = options.indentWidth;
|
||||
var indentWidth = indentWidthValue is int
|
||||
? indentWidthValue
|
||||
: int.parse(indentWidthValue.toString());
|
||||
var lineFeed = _parseLineFeed(options.linefeed);
|
||||
var result = newNodeResult(render(options.file,
|
||||
useSpaces: options.indentType == 'space',
|
||||
indentWidth: indentWidth,
|
||||
lineFeed: lineFeed));
|
||||
var result = newRenderResult(render(options.file,
|
||||
useSpaces: options.indentType != 'tab',
|
||||
indentWidth: _parseIndentWidth(options.indentWidth),
|
||||
lineFeed: _parseLineFeed(options.linefeed)));
|
||||
callback(null, result);
|
||||
} on SassException catch (error) {
|
||||
// TODO: populate the error more thoroughly if possible.
|
||||
callback(new NodeError(message: error.message), null);
|
||||
callback(newRenderError(message: error.message), null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,23 +56,25 @@ void _render(
|
||||
/// as possible.
|
||||
///
|
||||
/// [render]: https://github.com/sass/node-sass#options
|
||||
NodeResult _renderSync(NodeOptions options) {
|
||||
RenderResult _renderSync(RenderOptions options) {
|
||||
try {
|
||||
var indentWidthValue = options.indentWidth;
|
||||
var indentWidth = indentWidthValue is int
|
||||
? indentWidthValue
|
||||
: int.parse(indentWidthValue.toString());
|
||||
var lineFeed = _parseLineFeed(options.linefeed);
|
||||
return newNodeResult(render(options.file,
|
||||
useSpaces: options.indentType == 'space',
|
||||
indentWidth: indentWidth,
|
||||
lineFeed: lineFeed));
|
||||
return newRenderResult(render(options.file,
|
||||
useSpaces: options.indentType != 'tab',
|
||||
indentWidth: _parseIndentWidth(options.indentWidth),
|
||||
lineFeed: _parseLineFeed(options.linefeed)));
|
||||
} on SassException catch (error) {
|
||||
// TODO: populate the error more thoroughly if possible.
|
||||
throw new NodeError(message: error.message);
|
||||
jsThrow(newRenderError(message: error.message));
|
||||
throw "unreachable";
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the indentation width into an [int].
|
||||
int _parseIndentWidth(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) {
|
||||
|
13
lib/src/node/function.dart
Normal file
13
lib/src/node/function.dart
Normal file
@ -0,0 +1,13 @@
|
||||
// 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("Function")
|
||||
class JSFunction {
|
||||
@JS("Function")
|
||||
external JSFunction(String arg1, [String arg2, String arg3]);
|
||||
|
||||
external call(thisArg, [arg1, arg2]);
|
||||
}
|
@ -4,15 +4,25 @@
|
||||
|
||||
import 'package:js/js.dart';
|
||||
|
||||
import 'utils.dart';
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class NodeError {
|
||||
class RenderError {
|
||||
external String get message;
|
||||
external int get line;
|
||||
external int get column;
|
||||
external int get status;
|
||||
external String get file;
|
||||
|
||||
external factory NodeError(
|
||||
external factory RenderError._(
|
||||
{String message, int line, int column, int status, String file});
|
||||
}
|
||||
|
||||
RenderError newRenderError(
|
||||
{String message, int line, int column, int status, String file}) {
|
||||
var error = new RenderError._(
|
||||
message: message, line: line, column: column, status: status, file: file);
|
||||
setToString(error, () => "Error: $message");
|
||||
return error;
|
||||
}
|
@ -5,9 +5,13 @@
|
||||
import 'package:js/js.dart';
|
||||
|
||||
@JS()
|
||||
class NodeOptions {
|
||||
@anonymous
|
||||
class RenderOptions {
|
||||
external String get file;
|
||||
external String get indentType;
|
||||
external dynamic get indentWidth;
|
||||
external String get linefeed;
|
||||
|
||||
external factory RenderOptions(
|
||||
{String file, String indentType, indentWidth, String linefeed});
|
||||
}
|
@ -2,6 +2,8 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
|
||||
@JS('Buffer.from')
|
||||
@ -9,11 +11,11 @@ external _buffer(String source, String encoding);
|
||||
|
||||
@JS()
|
||||
@anonymous
|
||||
class NodeResult {
|
||||
external get buffer;
|
||||
class RenderResult {
|
||||
external Uint8List get css;
|
||||
|
||||
external factory NodeResult._({buffer});
|
||||
external factory RenderResult._({css});
|
||||
}
|
||||
|
||||
NodeResult newNodeResult(String css) =>
|
||||
new NodeResult._(buffer: _buffer(css, 'utf8'));
|
||||
RenderResult newRenderResult(String css) =>
|
||||
new RenderResult._(css: _buffer(css, 'utf8'));
|
23
lib/src/node/utils.dart
Normal file
23
lib/src/node/utils.dart
Normal file
@ -0,0 +1,23 @@
|
||||
// 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';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import 'function.dart';
|
||||
|
||||
/// Sets the `toString()` function for [object] to [body].
|
||||
///
|
||||
/// Dart's JS interop doesn't currently let us set toString() for custom
|
||||
/// classes, so we use this as a workaround.
|
||||
void setToString(object, String body()) =>
|
||||
_setToString.call(object, allowInterop(body));
|
||||
|
||||
final _setToString =
|
||||
new JSFunction("object", "body", "object.toString = body;");
|
||||
|
||||
/// Throws [error] like JS would, without any Dart wrappers.
|
||||
void jsThrow(error) => _jsThrow.call(error);
|
||||
|
||||
final _jsThrow = new JSFunction("error", "throw error;");
|
@ -28,8 +28,15 @@ dev_dependencies:
|
||||
http: "^0.11.0"
|
||||
js: "^0.6.0"
|
||||
node_preamble: "^1.1.0"
|
||||
stream_channel: "^1.0.0"
|
||||
test_descriptor: "^1.0.0"
|
||||
test_process: "^1.0.0-rc.1"
|
||||
test: "^0.12.5"
|
||||
test: "^0.12.24"
|
||||
xml: "^2.4.0"
|
||||
yaml: "^2.0.0"
|
||||
|
||||
dependency_overrides:
|
||||
test:
|
||||
git:
|
||||
url: git://github.com/dart-lang/test.git
|
||||
ref: node
|
||||
|
@ -2,6 +2,8 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
@TestOn('vm')
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
@TestOn('vm')
|
||||
|
||||
import 'package:package_resolver/package_resolver.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:test/test.dart';
|
||||
|
@ -6,22 +6,25 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:yaml/yaml.dart';
|
||||
|
||||
void hybridMain(StreamChannel channel) async {
|
||||
hybridMain(StreamChannel channel) async {
|
||||
if (!new Directory("build/npm").existsSync()) {
|
||||
throw "NPM package is not build. Run pub run grinder npm_package.";
|
||||
}
|
||||
|
||||
var lastModified = new DateTime(0);
|
||||
var entriesToCheck = new Directory("lib").listSync(recursive: true).toList()
|
||||
..add("pubspec.lock");
|
||||
..add(new File("pubspec.lock"));
|
||||
for (var entry in entriesToCheck) {
|
||||
if (entry is! File) continue;
|
||||
var entryLastModified = entry.lastModifiedSync();
|
||||
if (lastModified.isBefore(entryLastModified))
|
||||
lastModified = entryLastModified;
|
||||
if (entry is File) {
|
||||
var entryLastModified = entry.lastModifiedSync();
|
||||
if (lastModified.isBefore(entryLastModified)) {
|
||||
lastModified = entryLastModified;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastModified
|
||||
|
56
test/hybrid.dart
Normal file
56
test/hybrid.dart
Normal file
@ -0,0 +1,56 @@
|
||||
// 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 'dart:convert';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
|
||||
/// Creates a directory in the system temp directory and returns its path.
|
||||
Future<String> createTempDir() async => (await runHybridExpression(
|
||||
'(await Directory.systemTemp.createTemp("dart_sass_")).path')) as String;
|
||||
|
||||
/// Writes [text] to [path].
|
||||
Future writeTextFile(String path, String text) => runHybridExpression(
|
||||
'new File(message[0]).writeAsString(message[1])', [path, text]);
|
||||
|
||||
/// Recursively deletes the directoy at [path].
|
||||
Future deleteDirectory(String path) =>
|
||||
runHybridExpression('new Directory(message).delete(recursive: true)', path);
|
||||
|
||||
/// Runs [expression], which may be asynchronous, in a hybrid isolate.
|
||||
///
|
||||
/// Returns the result of [expression] if it's JSON-serializable.
|
||||
Future runHybridExpression(String expression, [message]) async {
|
||||
var channel = spawnHybridCode(
|
||||
'''
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
|
||||
hybridMain(StreamChannel channel, message) async {
|
||||
var result = await ${expression};
|
||||
channel.sink.add(_isJsonSafe(result) ? JSON.encode(result) : 'null');
|
||||
channel.sink.close();
|
||||
}
|
||||
|
||||
bool _isJsonSafe(object) {
|
||||
if (object == null) return true;
|
||||
if (object is String) return true;
|
||||
if (object is num) return true;
|
||||
if (object is bool) return true;
|
||||
if (object is List) return object.every(_isJsonSafe);
|
||||
if (object is Map) {
|
||||
return object.keys.every(_isJsonSafe) &&
|
||||
object.values.every(_isJsonSafe);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
''',
|
||||
message: message);
|
||||
|
||||
return JSON.decode((await channel.stream.first) as String);
|
||||
}
|
33
test/node_api.dart
Normal file
33
test/node_api.dart
Normal file
@ -0,0 +1,33 @@
|
||||
// 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.
|
||||
|
||||
/// This library exposes Dart Sass's Node.js API, imported as JavaScript, back
|
||||
/// 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 'dart:typed_data';
|
||||
|
||||
import 'package:sass/src/node/render_error.dart';
|
||||
import 'package:sass/src/node/render_options.dart';
|
||||
import 'package:sass/src/node/render_result.dart';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
export 'package:sass/src/node/render_error.dart';
|
||||
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"));
|
||||
|
||||
@JS("require")
|
||||
external Sass _require(String path);
|
||||
|
||||
@JS()
|
||||
class Sass {
|
||||
external RenderResult renderSync(RenderOptions args);
|
||||
external void render(RenderOptions args,
|
||||
void callback(RenderError error, RenderResult result));
|
||||
}
|
166
test/node_api_test.dart
Normal file
166
test/node_api_test.dart
Normal file
@ -0,0 +1,166 @@
|
||||
// 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.
|
||||
|
||||
@TestOn('node')
|
||||
@Tags(const ['node'])
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:js/js.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'ensure_npm_package.dart';
|
||||
import 'hybrid.dart';
|
||||
import 'node_api.dart';
|
||||
|
||||
String sandbox;
|
||||
String sassPath;
|
||||
|
||||
void main() {
|
||||
setUpAll(ensureNpmPackage);
|
||||
|
||||
setUp(() async {
|
||||
sandbox = await createTempDir();
|
||||
sassPath = p.join(sandbox, 'test.scss');
|
||||
await writeTextFile(sassPath, 'a {b: c}');
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
if (sandbox != null) await deleteDirectory(sandbox);
|
||||
});
|
||||
|
||||
group("renderSync()", () {
|
||||
test("renders a file", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath)), equals('''
|
||||
a {
|
||||
b: c;
|
||||
}'''));
|
||||
});
|
||||
|
||||
test("allows tab indentation", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, indentType: 'tab')),
|
||||
equals('''
|
||||
a {
|
||||
\t\tb: c;
|
||||
}'''));
|
||||
});
|
||||
|
||||
test("allows unknown indentation names", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, indentType: 'asdf')),
|
||||
equals('''
|
||||
a {
|
||||
b: c;
|
||||
}'''));
|
||||
});
|
||||
|
||||
group("linefeed allows", () {
|
||||
test("cr", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'cr')),
|
||||
equals('a {\r b: c;\r}'));
|
||||
});
|
||||
|
||||
test("crlf", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'crlf')),
|
||||
equals('a {\r\n b: c;\r\n}'));
|
||||
});
|
||||
|
||||
test("lfcr", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'lfcr')),
|
||||
equals('a {\n\r b: c;\n\r}'));
|
||||
});
|
||||
|
||||
test("unknown names", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, linefeed: 'asdf')),
|
||||
equals('a {\n b: c;\n}'));
|
||||
});
|
||||
});
|
||||
|
||||
group("indentWidth allows", () {
|
||||
test("a number", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, indentWidth: 10)),
|
||||
equals('''
|
||||
a {
|
||||
b: c;
|
||||
}'''));
|
||||
});
|
||||
|
||||
test("a string", () {
|
||||
expect(_renderSync(new RenderOptions(file: sassPath, indentWidth: '1')),
|
||||
equals('''
|
||||
a {
|
||||
b: c;
|
||||
}'''));
|
||||
});
|
||||
});
|
||||
|
||||
group("throws an error that", () {
|
||||
setUp(() => writeTextFile(sassPath, 'a {b: }'));
|
||||
|
||||
test("has a useful toString", () {
|
||||
var error = _renderSyncError(new RenderOptions(file: sassPath));
|
||||
expect(error.toString(), equals("Error: Expected expression."));
|
||||
});
|
||||
|
||||
test("has a useful message", () {
|
||||
var error = _renderSyncError(new RenderOptions(file: sassPath));
|
||||
expect(error.message, equals("Expected expression."));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group("render()", () {
|
||||
test("renders a file", () async {
|
||||
expect(await _render(new RenderOptions(file: sassPath)), equals('''
|
||||
a {
|
||||
b: c;
|
||||
}'''));
|
||||
});
|
||||
|
||||
test("throws an error that has a useful toString", () async {
|
||||
await writeTextFile(sassPath, 'a {b: }');
|
||||
|
||||
var error = await _renderError(new RenderOptions(file: sassPath));
|
||||
expect(error.toString(), equals("Error: Expected expression."));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
expect(error, isNull);
|
||||
completer.complete(UTF8.decode(result.css));
|
||||
}));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Asserts that rendering via [options] produces an error, and returns that
|
||||
/// error.
|
||||
Future<RenderError> _renderError(RenderOptions options) {
|
||||
var completer = new Completer<RenderError>();
|
||||
sass.render(options, allowInterop((error, result) {
|
||||
expect(result, isNull);
|
||||
completer.complete(error as RenderError);
|
||||
}));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Returns the result of rendering via [options] as a string.
|
||||
String _renderSync(RenderOptions options) =>
|
||||
UTF8.decode(sass.renderSync(options).css);
|
||||
|
||||
/// Asserts that rendering via [options] produces an error, and returns that
|
||||
/// error.
|
||||
RenderError _renderSyncError(RenderOptions options) {
|
||||
try {
|
||||
sass.renderSync(options);
|
||||
} catch (error) {
|
||||
return error as RenderError;
|
||||
}
|
||||
|
||||
throw "Expected renderSync() to throw an error.";
|
||||
}
|
@ -2,7 +2,9 @@
|
||||
// MIT-style license that can be found in the LICENSE file or at
|
||||
// https://opensource.org/licenses/MIT.
|
||||
|
||||
@TestOn('vm')
|
||||
@Tags(const ['node'])
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user